colorize <- function(x, color) {
 if (knitr::is_latex_output()) {
 sprintf("\\textcolor{%s}{%s}", color, x)
 } else if (knitr::is_html_output()) {
 sprintf("<span style='color: %s;'>%s</span>", 
 color,
 x)
 } else x
}

1 Kiến thức R cơ bản

đây là ký hiệu 鐕

đây là ký hiệu 񥌰

đây là ký hiệu 🄬

đây là ký hiệu 🅡

đây là ký hiệu 🆁

đây là ký hiệu Ⓡ

đây là ký hiệu ℝ

Mục đích của cuốn sách này không phải để bạn đọc trở thành một lập trình viên chuyên nghiệp. Cuốn sách được viết nhằm giúp bạn đọc có thể sử dụng R và thực hiện được mục đích của mình một cách nhanh nhất. Theo quan điểm của chúng tôi, R không phải là một ngôn ngữ thích hợp để bắt đầu cho học lập trình. Muốn trở thành một lập trình viên giỏi, bạn đọc nên bắt đầu với các ngôn ngữ lập trình cơ bản như Pascal, C++, Java, hay cũng có thể bắt đầu với ngôn ngữ Python.

Cách viết các dòng lệnh của R có thể nói là khá tùy tiện, thậm chí có thể làm cho những người có chuyên môn về lập trình cảm thấy khó chịu. Tuy nhiên, như đã đề cập trong phần giới thiệu của cuốn sách, R có các thế mạnh riêng mà các ngôn ngữ khác không có được và chúng tôi tin rằng R có thể giải quyết được tất cả những yêu cầu của bạn đọc từ những yêu cầu đơn giản đến những yêu cầu phức tạp nhất.

Cuốn sách dành cho cả các bạn đọc chưa từng làm quen với lập trình. Những bạn đọc đã có kinh nghiệm với lập trình có thể bỏ qua các phần không cần thiết.

1.1 Làm quen với các dòng lệnh cơ bản

1.1.1 Sử dụng R như một máy tính cầm tay

Để R hiểu và thực hiện được các yêu cầu của mình, bạn đọc cần phải giao tiếp với R theo ngôn ngữ mà phần mềm này có thể hiểu được. Câu lệnh đầu tiên và đơn giản nhất là hiển thị một giá trị lên màn hình Console. Bạn đọc hãy nhấp chuột vào cửa sổ Console, gõ trực tiếp đoạn câu lệnh như ở dưới và kết thúc câu lệnh bằng cách sử dụng phím Enter.

print("I am MFEer") # In ra màn hình R console dòng chữ I am MFEer

Bạn đọc có thể bắt đầu làm quen với các dòng lệnh của R bằng cách viết lên cửa sổ Console các công thức để thực hiện tính toán các phép toán dưới đây. R lúc này chỉ đơn giản là một máy tính cầm tay.

1+0.001 # phép tính cộng, số thập phân, số thập phân trong R sử dụng dấu "."
2*pi - 3 # số pi trong R được viết đơn giản là pi; pi nhận giá trị 3.1416...
exp(1)-exp(-1) # exp là hàm số mũ là lũy thừa của số e
log(3.2) # logarit cơ số tự nhiên của số 3.2
log(1000,10) # logarit cơ số 10 của số 1000

Bạn đọc có thể tiếp tục thực hành các câu lệnh cơ bản bằng cách tính toán kết quả của các biểu thức dưới đây

\[\begin{align} a) \cfrac{1}{4^{1/6}} \ \ \ b) \cfrac{7 - 4}{12 - 7} \ \ \ c) \sqrt{\cfrac{4}{22}} \ \ \ d) (12-5)^{4/3} \ \ \ e) ln\left( \cfrac{2 + 4}{2^5 -1} \right) \end{align}\]

Khi viết lên cửa sổ Console, R luôn thực hiện câu lệnh mỗi khi bạn đọc sử dụng phím Enter. Để viết hai hay nhiều câu lệnh trên một dòng khi sử dụng cửa sổ Console, bạn đọc hãy kết thúc mỗi câu lệnh bằng dấu “;”. Hãy thử câu lệnh ở dưới và quan sát R sẽ trả kết quả như thế nào

1+0.001; 2*pi - 3; exp(1)-exp(-1) # một dòng lệnh thực hiện ba câu lệnh (ba phép tính)
## [1] 1.001
## [1] 3.283185
## [1] 2.350402

Khi bạn đọc viết các câu lệnh đơn giản, sử dụng nhiều câu lệnh trên một dòng có thể hạn chế việc dùng phím Enter nhiều lần, tuy nhiên chúng tôi khuyên bạn đọc khi muốn thực hiện nhiều câu lệnh khác nhau hãy sử dụng cửa sổ Script thay vì viết câu lệnh trực tiếp lên cửa sổ Console. Cách viết câu lệnh trên cửa sổ Script và cho các câu lệnh chạy sẽ được thảo luận ở phần sau.

1.1.2 Sử dụng cửa sổ Script để viết câu lệnh R

Cách tốt nhất bạn đọc nên sử dụng khi viết câu lệnh đó là sử dụng cửa sổ Script. Để mở cửa sổ Script bạn đọc có thể tìm trên thanh công cụ theo trình tự \(File\) \(\rightarrow\) \(New\) \(file\) \(\rightarrow\) \(R\) \(Script\), hoặc bạn đọc sử dụng tổ hợp phím tắt “Ctrl + Shift + N”. Khi viết câu lệnh trên cửa sổ Script, R chỉ thực hiện câu lệnh khi bạn đọc yêu cầu. Do đó, bạn đọc có thể sử dụng cửa sổ Script để viết các chương trình lớn, có nhiều dòng lệnh kế tiếp nhau.

Sau khi mở của sổ Script, bạn đọc có thể viết các dòng lệnh và sử dụng phím Enter để xuống dòng và không cần quan tâm đến việc R có chạy câu lệnh đó hay không. Trong một dòng lệnh trên cửa sổ Script mỗi khi bạn đọc sử dụng dấu ngắt câu lệnh “;” R vẫn hiểu rằng bạn đọc đang viết hai câu lệnh khác nhau trên một dòng:

1+0.001 ; 2*pi - 3 ; exp(1)-exp(-1)
log(3.2) 
log(1000,10)

Để chạy các dòng lệnh trên cửa sổ Script, bạn đọc sử dụng con trỏ và click chuột trái vào nút Run nằm ở phía góc trên bên phải của cửa sổ này hoặc sử dụng tổ hợp phím “Ctrl + Enter”. Để chạy một dòng lệnh riêng lẻ trên Script, bạn đọc di chuyển con trỏ đến dòng lệnh đó và thực hiện thao tác chạy. Để chạy nhiều dòng lệnh trên cửa sổ Script, bạn đọc sử dụng chuột trái lựa chọn các dòng lệnh mình muốn chạy và sau đó thực hiện thao tác chạy. Khi bạn đọc lựa chọn nhiều dòng lệnh một lúc để chạy, R sẽ thực hiện các câu lệnh lần lượt theo thứ tự từ trên xuống dưới và từ bên trái qua bên phải nếu một dòng có nhiều câu lệnh.

Lưu ý, khi bạn đọc viết một chương trình bao gồm nhiều dòng lệnh, bạn thường phải sử dụng ngôn ngữ thông thường như tiếng Việt, tiếng Anh, …, để ghi chú lại các dòng lệnh hoặc nhóm các dòng lệnh đó có ý nghĩa là gì. Việc này giúp cho chính bản thân bạn đọc khi xem lại các dòng lệnh R của mình và những người khác khi đọc các dòng lệnh, có thể hiểu được nhanh hơn bạn đọc đang làm gì. Các câu ghi chú đó theo ngôn ngữ lập trình được gọi là các câu \(comment\). Để R sẽ hiểu được đó là các câu ghi chú bạn đọc cần phải thêm dấu “#” trước các câu đó.

# Đây là cách tính xấp xỉ số e
n<-1000
cat("e = ", (1+1/n)^n) # Khi n càng lớn thì kết quả càng chính xác

1.2 Biến trong R

Biến là khái niệm cơ bản nhất trong mọi ngôn ngữ lập trình. Có bốn loại biến cơ bản trong R: biến kiểu số, biến kiểu ký tự, biến kiểu logic, và biến kiểu thời gian. Một số tài liệu khác khi viết về ngôn ngữ lập trình R phân loại biến thành nhiều kiểu hơn, có thêm kiểu số nguyên, kiểu factor,… Theo quan điểm của chúng tôi, phân loại biến quá chi tiết sẽ gây khó khăn cho bạn đọc, nhất là với bạn đọc mới làm quen với lập trình. Trong các phần tiếp theo của cuốn sách, chúng tôi sẽ thảo luận về mỗi kiểu biến cụ thể.

Để tạo một biến trong R và gán giá trị cho biến đó, bạn đọc sử dụng một trong ba cách như sau

# Cách thứ nhất
tenbien <- giatri # dấu "<-" là dấu gán giá trị
# Cách thứ hai
tenbien -> giatri
# Cách thứ ba
tenbien = giatri # dấu "=" cũng được sử dụng để gán giá trị

Trong đó \(tenbien\) là tên của biến mà bạn muốn đặt, \(giatri\) là giá trị mà bạn muốn gán cho biến. Ký tự gán giá trị <- được sử dụng trong các phiên bản R đầu tiên. Gán giá trị cho biến sử dụng ký tự -> hiếm khi được dùng. Từ năm 2001 trở đi, dấu = cũng có thể được sử dụng để gán giá trị cho biến. Tuy nhiên dấu = có thể gây nhầm lẫn sau này khi bạn đọc sử dụng song song với ký hiệu == và ký hiệu = trong truyền giá trị cho tham số khi viết hàm số. Trong cuốn sách này, chúng tôi luôn sử dụng <- để gán giá trị cho biến. Các ví dụ về tạo biến và gán giá trị cho biến ở trong các dòng lệnh phía dưới.

# Cách thứ nhất
x <- 3  # tạo một biến tên là x có giá trị bằng 3
# Cách thứ hai
"MFE" -> y # tao một biến tên là y có giá trị bằng đoạn ký tự "MFE"
# Cách thứ ba
z = 1 + 2 # tạo một biến tên là z và nhận giá trị bằng kết quả của phép cộng

Trong các câu lệnh ở trên, \(x\), \(y\) hay \(z\) là tên biến. Quy tắc đặt tên biến hay rộng hơn là tên một đối tượng trong R cần tuân theo các quy tắc sau:

  1. Tên biến có thể là tổ hợp của tất cả các chữ cái viết hoa, chữ cái viết thường và các chữ số.

  2. Trong tên biến có thể chứa hai ký tự đặc biệt là “.” và “_“.

  3. Tên biến không được phép bắt đầu bằng số hoặc ký tự “_“.

  4. Không được dùng từ khóa để đặt tên biến.

Để kiểm tra các quy tắc ở trên, bạn đọc có thể chạy các câu lệnh tạo biến dưới đây và xem dòng lệnh nào báo lỗi và dòng lệnh nào không báo lỗi.

x1 <- 3 # biến tên x1 sẽ được tạo với giá trị bằng 3
1x <- 3 # sẽ báo lỗi vì tên biến không được phép bắt đầu bằng số
.x <- 3 # biến tên .x sẽ được tạo với giá trị bằng 3
_x <- 3 # sẽ báo lỗi vì tên biến không được phép bắt đầu bằng số

Lưu ý rằng R có phân biệt chữ viết hoa với chữ viết thường trong tên biến. Chúng ta có thể sử dụng \(x\) để đặt tên và sau đó dùng \(X\) để đặt tên cho một biến khác:

x<-3 # tạo một biến tên x nhận giá trị bằng 3
X<-5 # tạo một biến tên X nhận giá trị bằng 5
X-x # hiệu số nhận giá trị bằng 2 do x và X là khác nhau

Để biết danh sách các tên biến và các biến nhận giá trị nào, ngoài việc in giá trị biến lên của sổ Console bạn đọc có thể sử dụng cửa sổ Environment ở góc phía trên bên phải của Rstudio. Để xóa một biến hoặc một đối tượng nào đó có tên trên cửa sổ Environment, bạn đọc sử dụng lệnh rm()

x # Console sẽ in ra giá trị bằng 3
rm(x) # xóa biến x khỏi Rstudio đang chạy
x # sau khi xóa biến x sẽ không còn tồn tại nên R sẽ báo lỗi

Một điều cũng cần lưu ý khi đặt tên biến, hay tên bất kỳ một đối tượng nào khác trong R, đó là tên biến không được phép trùng với các từ khóa. Danh sách các từ khóa thường sử dụng trong R nằm trong bảng dưới đây

(#tab:unnamed-chunk-9)Danh sách các từ khóa không được dùng để đặt tên
Từ khóa Sử dụng trong ngữ cảnh
If, else Câu lệnh điều kiện
for, while, in , repeat Vòng lặp
function Khai báo hàm số
break, next Điều khiển vòng lặp
TRUE, FALSE Tên các biến logic
Inf, -Inf, NaN, NA Các biến kiểu số dạng đặc biệt

Chúng ta sẽ thảo luận về từng kiểu biến trong các phần tiếp theo.

1.2.1 Biến kiểu số

Biến kiếu số, hay còn được gọi là kiểu \(numeric\), là các biến nhận giá trị kiểu số thập phân. Để tạo một biến kiểu số, bạn đọc hãy khởi tạo biến bằng cách gán một giá trị kiểu số cho tên biến mà bạn muốn đặt. Đây cũng là cách tạo biến chung trong R.

x <- 5 # do 5 là giá trị kiểu số nên R sẽ hiểu x là biến kiểu số

Để kiểm tra xem \(x\) có phải là biến kiểu số không, bạn đọc sử dụng hàm is.numeric(). Hàm số này trả lại giá trị là kiểu logic. Giá trị \(TRUE\) cho biết biến được hỏi đúng là kiểu số; giá trị \(FALSE\) cho biết biến được hỏi không phải là kiểu số. Ngoài cách sử dụng hàm is.numeric(), bạn đọc cũng có thể sử dụng hàm class(). Cách sử dụng hai hàm này như sau:

is.numeric(x) # do 5 là giá trị kiểu số nên R trả lời TRUE
## [1] TRUE
class(x) # do 5 là giá trị kiểu số nên R sẽ hiểu x là biến kiểu số (numeric)
## [1] "numeric"
x<-"abc" # thử với biến x không phải là kiểu số
is.numeric(x) # x không phải giá trị kiểu số nên kết quả là FALSE
## [1] FALSE

Trong phép gán cho giá trị của biến \(x\) như ở trên, mặc dù giá trị khởi tạo (số 5) là số nguyên nhưng R vẫn mặc định cho rằng \(x\) là số thập phân. Để tạo một biến kiểu số nguyên trong R, bạn đọc cần phải sử dụng chữ “L” phía sau số nguyên đó. Chữ L là viết tắt cho “Long” nghĩa là số nguyên kiểu \(Long\) trong các ngôn ngữ lập trình cơ bản. Số nguyên kiểu \(Long\) là các số nguyên cần 32 bytes (1 byte là 1 ô chứa số 0 hoặc 1) để lưu và nhận \(2^{32}\) giá trị từ −2,147,483,648 (\(-2^{31}\)) đến 2,147,483,647 (\(2^{31}-1\)). Để tạo biến \(x\) nhận giá trị là số nguyên 5 chúng ta viết như sau:

x<-5L # 5L nghĩa là số nguyên 5, L là viết tắt của Long
class(x) # x là số tự nhiên
## [1] "integer"
is.numeric(x) # x không còn là số thập phân, nhưng vẫn là kiểu số
## [1] TRUE

Phân biệt số nguyên (integer) và số thập phân (numeric) trong các ngôn ngữ lập trình có ý nghĩa khi bạn đọc cần tiết kiệm bộ nhớ cho chương trình. Trong R, khi sử dụng số thập phân thay cho số nguyên, dung lượng bộ nhớ máy tính sẽ tăng gấp 2 lần. Hình vẽ dưới đây mô tả dung lượng bộ nhớ cần sử dụng cho các véc-tơ chứa các số nguyên và các véc-tơ chứa các số thập phân với độ dài (số lượng phần tử trong véc-tơ) từ 1 đến 100. Không có sự khác biệt về bộ nhớ cho véc-tơ có độ dài dưới 10 nhưng khi véc-tơ có độ dài từ 10 trở lên, véc-tơ kiểu số thập phân cần trung bình khoảng 2 lần bộ nhớ so với véc-tơ kiểu số nguyên.

Các phép tính toán thông thường khi sử dụng với biến kiểu số được liệt kê trong bảng dưới đây

(#tab:unnamed-chunk-14)Các phép toán cơ bản với biến kiểu số
Ký hiệu Phép tính
+ Phép tính cộng
- Phép tính trừ
* Phép tính nhân
/ Phép tính chia
^ Phép tính lũy thừa
exp() Phép tính lũy thừa cơ số e
log() Phép lấy loga cơ số tự nhiên
log(.,a) Phép lấy loga cơ số a
%% Phép lấy phần dư trong phép chia
%/% Phép lấy phần nguyên của kết quả trong phép chia

Lưu ý rằng các phép toán \(%%\)\(%\%\) có thể thực hiện được với cả số kiểu thập phân

6.5 %% 2 # phần dư của phép chia 6.5 cho 2, R sẽ trả kết quả là 1.5
## [1] 0.5
6.5 %/% 2 # phần nguyên của kết quả của phép chia 6.5 cho 2
## [1] 3

Trong R có cách viết biến kiểu số theo kiểu khoa học và các giá trị số đặc biệt mà bạn đọc cũng nên ghi nhớ:

(#tab:unnamed-chunk-15)Các giá trị số đặc biệt
Loại số Ý nghĩa
1.2e+8 nghĩa là nhân số 1.2 với 10 lũy thừa 8
1.2e-5 nghĩa là nhân số 1.2 với 10 lũy thừa -5
Inf Số dương vô cùng
-Inf Số âm vô cùng
NaN là kết quả của các phép tính không có nghĩa, viết tắt của Not a Number

Bạn đọc có thể thử tính toán trên các giá trị đặc biệt

1/0 # kêt quả của 1/0 là dương vô cùng (Inf)
(-1)/0 # kêt quả của 1/0 là âm vô cùng (-Inf)
Inf - 10^10 # Trong các phép tính có Inf sẽ dẫn đến kết quả là Inf
1/0 + (-1)/0 # Inf + (-Inf) là không thể xác định được (NaN)
log(-2) # Kết quả của các phép tính không có nghĩa là NaN

1.2.2 Biến kiểu logic

Biến kiểu logic là kiểu biến đơn giản nhất nhưng lại là kiểu biến quan trọng nhất trong mọi ngôn ngữ lập trình. Biến kiểu logic chỉ nhận một trong hai giá trị là \(TRUE\) hoặc \(FALSE\). Do R phân biệt chữ viết hoa và chữ viết thường nên bạn đọc lưu ý khi viết giá trị cho biến kiểu logic là hoàn toàn các chữ cái viết hoa. Để tạo một biến kiểu logic, bạn đọc tạo đặt tên biến và gán một trong hai giá trị logic cho biến đó. Việc này hoàn toàn giống như khi tạo một biến kiểu số

x<-TRUE

Biến kiểu logic có thể đặt trong các phép tính toán giống như biến kiểu số. Khi gặp một công thức có bao gồm cả biến kiểu số và biến kiểu logic, R sẽ đổi biến kiểu logic nhận giá trị \(TRUE\) thành số 1 và biến kiểu logic có giá trị \(FALSE\) thành số 0 để thực hiện phép tính toán.

FALSE + TRUE * 10 # Sẽ cho kết quả giống như 0 + 1 * 10
## [1] 10

Trong thực tế, ít khi chúng ta cần phải khởi tạo giá trị cho biến kiểu logic như trên, mà biến kiểu logic thường nhận được từ kết quả các phép so sánh trong R. Các phép toán so sánh này được liệt kê trong bảng dưới đây

(#tab:unnamed-chunk-18)Các phép so sánh cơ bản
Phép so sánh Ý nghĩa
< Có nhỏ hơn không?
> Có lớn hơn không?
<= Có nhỏ hơn hoặc bằng không?
>= Có lớn hơn hoặc bằng không?
== Có bằng nhau không?
!= Có khác nhau không?

Ngoài ra, các biến kiểu logic còn là kết quả của việc kết hợp nhiều biến kiểu logic khác bằng các toán tử logic. Các toán tử logic bao gồm có “Và”, “Hoặc” và toán tử “Phủ định”

(#tab:unnamed-chunk-19)Các toán tử logic
Toán tử logic Ý nghĩa
& Toán tử Và; A&B đọc là A và B

Bạn đọc cần ghi nhớ quy tắc kết hợp các biến kiểu logic bằng các toán tử logic như bảng dưới đây

(#tab:unnamed-chunk-20)Kết hợp biến logic với các toán tử logic
Kết hợp Kết quả
!TRUE FALSE
!FASLE TRUE
TRUE & TRUE TRUE
TRUE & FALSE FALSE
FALSE & TRUE FALSE
TRUE | TRUE TRUE
TRUE | FALSE TRUE
FALSE | TRUE TRUE

Như chúng tôi đã đề cập ở phần trên, các biến kiểu logic khi đặt trong các biểu thức tính toán sẽ được tự động đổi sang biến kiểu số trước khi thực hiện phép tính. Ngược lại, khi biến kiểu số xuất hiện trong các biểu thức có toán tử logic, biến kiểu số cũng sẽ được chuyển sang kiểu logic. Tuy nhiên, bạn đọc lưu ý rằng: chỉ có số 0 khi đặt trong biểu thức có toán tử logic mới được chuyển thành \(FALSE\), mọi số khác 0 khi đổi sang kiểu logic đều được chuyển thành \(TRUE\)

Bạn đọc có thể thực hành việc tính toán trên các toán tử logic như dưới đây. Trước khi sử dụng R để xem kết quả, hãy thử suy nghĩ xem các biểu thức sau đây cho kết quả như thế nào.

# 1.
(1<=2) | (2<=3)
# 2.
(1<=2) + (2<=3)
# 3.
((1<=2) * (2^2 == 4)) | (2!=3) # 
# 4.
!((1<=2) * (2^2 == 4)) & !(2!=3) # 
# 5.
((2 + 2) | (2 - 2)) & !(2 ^ 2) #

1.2.3 Biến kiểu chuỗi ký tự

Trong R, biến kiểu chuỗi ký tự được gọi là kiểu character. Biến kiểu chuỗi ký tự tương tự như biến kiểu xâu ký tự (thường được gọi là string) trong các ngôn ngữ lập trình cơ bản. Biến kiểu chuỗi ký tự có thể chỉ ngắn gọn là một ký tự trống, một chữ cái, đôi khi có thể là cả một câu văn, và cũng có thể là cả một đoạn văn bản dài. Khi làm việc với biến kiểu ký tự, bạn đọc hãy luôn ghi nhớ rằng R phân biệt chữ viết hoa và chữ viết thường.

Để tạo một biến có kiểu ký tự trong R, bạn đọc cần tạo tên biến và gán cho biến giá trị kiểu chuỗi ký tự. R sẽ hiểu một biến là chuỗi ký tự khi chuỗi ký tự đó nằm trong dấu ngoặc kép “” hoặc trong dấu ngoặc đơn (’’).

x<-"Ice cream" # "Ice cream" với chữ I viết hoa sẽ khác "ice cream" khi i là chữ thường
x == "ice cream" # sẽ trả ra giá trị là FALSE
## [1] FALSE

Để biết một biến có phải kiểu chuỗi ký tự không, bạn đọc có thể dùng hàm is.character() hoặc hàm class()

## [1] TRUE
## [1] "character"

Khi xử lý biến kiểu chuỗi ký tự, bạn đọc nên sử dụng các hàm số đã được xây dựng sẵn. Bảng dưới đây liệt kê danh sách các hàm thường sử dụng và kết quả đầu ra của các hàm này

(#tab:unnamed-chunk-24)Các hàm thường sử dụng khi làm việc với chuỗi ký tự
Hàm số Ý nghĩa
nchar(x) Cho biêt biến x dạng chuỗi ký tự có bao nhiêu ký tự
paste(x1,x2,sep = a) Ghép hai chuỗi ký tự x1 và x2 thành một chuỗi ký tự cách nhau chuỗi ký tự a
toupper(x) Chuyển tất cả các chữ viêt thường trong x thành chữ viết hoa
tolower(x) Chuyển tất cả các chữ viết hoa trong x thành chữ viết thường
chartr(a,b,x) Thay thế trong x: từng ký tự trong chuỗi a tương ứng bằng từng ký tự trong chuỗi b, a và b phải có độ dài bằng nhau
substr(x,k,n) Lấy ra chuỗi ký tự con từ x, lấy từ ký tự thứ k đến ký tự thứ n
sub(a, b, x) Đoạn ký tự a đầu tiên trong x sẽ được thay thế bằng đoạn ký tự b
gsub(a, b, x) Tất cả các đoạn ký tự giống a trong x sẽ được thay thế bằng b
grepl(a,x) Trả lại giá trị là biến TRUE nếu đoạn ký tự a nằm trong biến x

Bạn đọc có thể thử các hàm liệt kê trong bảng ở trên và quan sát giá trị trả ra của các hàm để hiểu cách áp dụng:

x1<-"I am an Actuary"; x2<-"I am Vietnamese"
nchar(x1) # cho biết x1 có bao nhiêu ký tự, tính cả các khoảng trống
paste(x1, x2, sep = " and ") # ghép x1 và x2 lại với nhau và thêm " and " vào giữa
toupper(x1); tolower(x1) # chuyển tất cả các ký tụ sang viết hoa/viết thường
chartr("an","bm",x1) # thay tất cả các chữ "a" trong x1 bằng "b" và "n" bằng "m"
substr(x1, 9, 15) # lấy ra đoạn ký tự từ ký tự thứ 9 (chữ A) đến ký tự thứ 15 (chữ "y")
sub("a", "XYZ", x1) # thay chữ "a" đầu tiên trong x1 bằng đoạn "XYZ"
gsub("a", "XYZ", x1) # thay tất cả chữ "a" trong x1 bằng đoạn "XYZ"
grepl("Vietnam", x2) # cho biết đoạn ký tự "Vietnam" có nằm trong x2 hay không 

Nhìn chung xử lý biến kiểu chuỗi ký tự sẽ khó khăn hơn so với xử lý biến kiểu số. Để thực hiện được các yêu cầu phức tạp hơn, bạn đọc có thể kết hợp các hàm số ở trên để có hiệu quả tốt hơn, hoặc sử dụng các thư viện được phát triển dành riêng cho biến kiểu chuỗi ký tự. Chúng tôi thường sử dụng thư viện \(stringr\) khi xử lý biến kiểu chuỗi ký tự. Các hàm hữu ích trong thư viện \(stringr\) sẽ được thảo luận khi chúng ta làm việc với dữ liệu chứa các biến kiểu chuỗi ký tự.

Một kiểu biến bạn đọc cũng thường gặp khi làm việc với dữ liệu trong R là biến hay véc-tơ kiểu factor. Biến kiểu factor cũng có thể được hiểu là biến kiểu chuỗi ký tự nhưng được R lưu trữ dưới dạng tiết kiệm bộ nhớ. Chúng ta sẽ thảo luận kỹ hơn về biến kiểu factor khi làm việc với véc-tơ kiểu chuỗi ký tự.

1.2.4 Biến kiểu thời gian

Trong R có hai kiểu biến thời gian là biến kiểu ngày tháng (\(Date\)) và biến kiểu thời gian chi tiết (\(POSIXct\)). Thời gian POSIX hay còn được biết đến với tên gọi là thời gian Unix là một cách quy ước về thời gian của một thời điểm cụ thể được tính bằng số giây từ cột mốc thời gian Unix đến thời điểm đó. Cột mốc thời gian Unix được các kỹ sư xây dựng hệ điều hành Unix lựa chọn là thời điểm 0 giờ, 0 phút, 0 giây, ngày 01 tháng 01 năm 1970 theo giờ phối hợp quốc tế (giờ UTC). Chữ “ct” là viết tắt của canlendar time. Bạn đọc cũng có thể gặp biến kiểu thời gian chi tiết trong R dưới dạng \(POSIXlt\) trong đó “lt” là chữ viết tắt của local time. Sự khác biệt của biến kiểu \(POSIXct\)\(POSIXlt\) chỉ là cách R lưu trữ các biến này dưới dạng số nguyên hay dưới dạng véc-tơ. Trong cuốn sách này khi nói đến biến kiểu thời gian chúng tôi luôn sử dụng biến kiểu \(POSIXct\).

Để tạo một biến kiểu thời gian trong R, bạn đọc sử dụng hàm as.Date() cho biến kiểu ngày tháng và hàm as.POSIXct() cho biến kiểu thời gian chi tiết:

date1<-as.Date("2023-08-31") # biến date1 nhận giá trị là ngày 31 tháng 08 năm 2023
time1<-as.POSIXct("2023-08-31 16:41:30") # biến time1 là 16 giờ, 41 phút, 30 giây ngày 31 tháng 08 năm 2023

Khi xử lý biến kiểu thời gian, bạn đọc nên đổi sang dạng số hoặc lưu biến kiểu thời gian dưới dạng một véc-tơ số lưu lại các thành phần của thời gian theo một thứ tự nhất định. Hàm as.numeric() sẽ đổi các biến kiểu ngày tháng hoặc thời gian chi tiết ra thành số ngày (đối với biến kiểu ngày tháng) hoặc số giây (đối với biến kiểu thời gian chi tiết) tính từ mốc thời gian Unix.

as.numeric(date1) # cho biết số ngày tính từ 01/01/1970 đến date1
## [1] 19600
time2<-as.POSIXct("1970-01-01 07:00:30")
as.numeric(time2) # cho biết số giây tính từ 7 giờ, 0 phút, 0 giây ngày 01/01/1970 đến time2
## [1] 30

Do múi giờ UTC của Việt Nam là \(UTC + 7\) nên thời điểm tính làm mốc sẽ là 7 giờ, 0 phút, 0 giây ngày 01 tháng 01 năm 1970. Điều này giải thích tại sao khi đổi biến time2 thành dạng số ta sẽ thu được kết quả là 30 giây. Khi sử dụng các hàm as.Date() hoặc as.POSIXct() giá trị được đưa vào phải là biến dạng chuỗi ký tự được viết theo đúng quy tắc “YYYY-MM-DD” và “YYYY-MM-DD hh:mm:ss”. Trong trường hợp chuỗi ký tự được đưa vào không đúng định dạng, bạn đọc cần phải thông báo cho R biết định dạng của biến chuỗi ký tự đó bằng cách sử dụng thêm tùy biến \(format\). Bạn đọc có thể tham khảo cách khai báo định dạng của biến chuỗi ký tự trong các hàm as.Date hoặc as.POSIXct() như sau

date1<-as.Date("02/27/92", format = "%m/%d/%y") # date1 sẽ nhận giá trị là ngày 27 tháng 02 năm 1992
date2<-as.Date("02 Jan 2010", format = "%d %b %Y") # ngày 02 tháng 01 năm 2010

Trong rất nhiều trường hợp, biến kiểu thời gian sẽ được lấy từ các nguồn khác nhau vào R và được lưu dưới dạng số tự nhiên. Điển hình là khi bạn đọc lấy dữ liệu từ các file được lưu từ phần mềm Microsoft Excel. Các hàm as.Date()as.POSIXct() cũng có thể chuyển giá trị số biến kiểu ngày tháng và biến kiểu thời gian chi tiết. Bạn đọc cần sử dụng thêm tùy biến \(origin\) trong các hàm này để quy định mốc thời gian.

date1<-as.Date(19000, origin = "1970-01-01")
time1<-as.POSIXct(10^9, origin = "1970-01-01 07:00:00")

Sau khi chạy các câu lệnh ở trên, biến \(date1\) tương ứng với ngày thứ 19000 tính từ mốc ngày 1 tháng 1 năm 1970 và biến \(time1\) tương ứng với thời điểm giây thứ 1 tỷ tính từ 07 giờ (đúng) ngày 1 tháng 1 năm 1970.

Vấn đề thường gặp phải đó là cách chuyển đổi từ thời gian thành số của phần mềm lưu dữ liệu gốc có mốc thời gian khác với R. Chẳng hạn như biến kiểu thời gian từ Microsoft Excel khi chuyển đổi thành số sử dụng mốc thời gian là ngày 30 tháng 12 năm 1899. Giả sử khi bạn đọc lấy một biến thời gian từ Microsoft Excel vào R và thấy giá trị là 45.678. Nếu không sử dụng mốc thời gian của Microsoft Excel để chuyển đổi, giá trị thời gian nhận được sẽ không đúng.

date1<-as.Date(45678, origin = "1970-01-01")
date1 # date1 sẽ nhận giá trị SAI khi nhận định mốc thời gian là ngày 01 tháng 01 năm 1970
## [1] "2095-01-23"
date2<-as.Date(45678, origin = "1899-12-30")
date2 # date2 sẽ nhận giá trị ĐÚNG do khi chuyển đổi đã dùng đúng mốc thời gian của Excel
## [1] "2025-01-21"

Nguyên tắc cơ bản khi xử lý và tính toán với biến kiểu thời gian trong R là luôn luôn đổi biến sang kiểu số nguyên hoặc đổi một biến kiểu thời gian thành một véc-tơ chứa các thành phần ngày, tháng, năm, giờ, phút, giây ở dạng số. Để tách biến kiểu ngày tháng ra thành ngày, tháng, năm bạn đọc có thể sử dụng hàm sub.str() để lấy ra các đoạn ký tự chứa giá trị ngày, tháng, và năm rồi sau đó sử dụng hàm as.numeric() để đổi các biến thành biến kiểu số:

year<-as.numeric(substr(date2,1,4)) # sẽ lấy ra đoạn ký tự từ 1-4 trong date2 sau đó đổi đoạn ký tự thành số
month<-as.numeric(substr(date2,6,7)) # sẽ lấy ra đoạn ký tự từ 6-7 trong date2 sau đó đổi đoạn ký tự thành số
day<-as.numeric(substr(date2,9,10)) # sẽ lấy ra đoạn ký tự từ 9-10 trong date2 sau đó đổi đoạn ký tự thành số

Xử lý biến kiểu ngày tháng và biến kiểu thời gian phức tạp hơn so với xử lý biên kiểu số và thường cần thêm các thư viện bổ sung. Thư viện thường chúng tôi được sử dụng khi làm việc với biến kiểu thời gian là thư viện \(lubridate\) và thư viện \(hms\). Bạn đọc sẽ sử dụng các thư viện này để thực hành với biến kiểu thời gian trong chương phân tích dữ liệu.

1.3 Véc-tơ trong R

Trong phần này của cuốn sách chúng tôi sẽ giới thiệu các khái niệm cơ bản về véc-tơ để bạn đọc có hiểu biết cơ bản nhất về khái niệm của véc-tơ và thế mạnh của R khi làm việc với véc-tơ. Trong tất cả các phần tiếp theo của cuốn sách đều có liên quan đến đối tượng véc-tơ do đó đi quá sâu vào chi tiết trong phần này là không thực sự cần thiết.

1.3.1 Tại sao xử lý véc-tơ là thế mạnh của R?

Véc-tơ là một tập hợp các phần tử có cùng kiểu được sắp xếp theo một thứ tự nhất định. Thứ tự của một phần tử trong véc-tơ thường được gọi là chỉ số. Phần tử đầu tiên trong một véc-tơ của R có chỉ số là 1. Bạn đọc hãy lưu ý điều này bởi trong một vài ngôn ngữ khác chỉ số của phần tử đầu tiên trong véc-tơ sẽ là 0. Vec-tơ là đối tượng quan trọng nhất trong R và xử lý vec-tơ chính là một thế mạnh của R mà đa số các ngôn ngữ cơ bản khác không đáp ứng được.

Khi bạn đọc làm việc với dữ liệu, các thao tác biến đổi dữ liệu thường sẽ là biến đổi đồng thời các giá trị trên cùng một hàng hoặc một cột dữ liệu. Hiếm khi các thao tác này được thực hiện với một giá trị riêng lẻ. Đối tượng véc-tơ là một công cụ hiệu quả để thực hiện các công việc này. Hiệu quả ở đây không chỉ bao gồm sự tiện lợi khi viết các câu lệnh, mà còn hiệu quả ở cả thời gian thực hiện tính toán. Trong phần Lập trình với R chúng tôi sẽ thảo luận kỹ hơn về hiệu quả về thời gian tính toán. Hãy nói về sự tiện lợi khi sử dụng véc-tơ trước. Chúng tôi thực hiện một phân tích trên dữ liệu có tên là \(trump\_tweets\) nằm trong thư viện \(dslabs\) bằng cách chạy một đoạn lệnh sau

library(dslabs) # cần gọi thư viện dslabs chứa dữ liệu trump_tweets
barplot(table(as.factor(as.numeric(substr(trump_tweets$created_at,12,13)))),
        main = "Tổng thống Trump viết tweet vào thời gian nào trong ngày", col = "lightskyblue")

Dữ liệu \(trump\_tweets\) là dữ liệu chứa hơn 20 nghìn câu “tweets” của cựu tổng thống Mỹ Donald Trump trong khoang thời gian từ 2009 đến 2017. Đoạn câu lệnh trên thực hiện một phân tích cho biết kết quả là Donald Trump có thói quen viết “tweets” vào thời gian nào trong ngày. Kết quả này thu được bằng việc thực hiện 1 loạt các phép biến đổi và tính toán cột có tên là \(created\_at\) của dữ liệu:

  1. Lấy ra đoạn ký tự chứa giá trị là giờ của cột \(created\_at\) (dùng hàm substr()).
  2. Chuyển đổi dữ liệu kiểu chuỗi ký sang kiểu số (dùng hàm as.numeric()).
  3. Chuyển đổi dữ liệu kiểu số sang kiểu factor (dùng hàm as.factor())
  4. Tổng hợp lại dữ liệu kiểu factor theo các nhóm (dùng hàm table())
  5. Vẽ đồ thị kiểu \(barplot\) để người đọc hiểu về dữ liệu một cách nhanh chóng và trực quan hơn.

Để đi từ cột dữ liệu \(created\_at\) kiểu \(POSIXct\) đến kết quả là đồ thị dạng \(barplot\) mà chỉ cần một dòng lệnh là việc gần như không thể đối với đa số các ngôn ngữ lập trình. Các ngôn ngữ lập trình cơ bản chỉ cho phép người sử dụng tác động đển từng phần tử của véc-tơ một cách lần lượt và riêng lẻ. Trái lại, khi bạn đọc thực hiện một phép biến đổi hay tính toán trên đối tượng là véc-tơ trong R, các phép tính toán hay biến đổi này sẽ được thực hiện một cách đồng thời cho tất cả các phần tử trong véc-tơ. Ngoài việc giúp cho các câu lệnh trở nên đơn giản, dể hiểu, R cũng được phát triển để những tính toán trên véc-tơ được thực hiện theo cơ chế song song. Cơ chế song song hiểu một cách đơn giản là việc thực hiện các phép toán trên các phần tử của một véc-tơ sẽ diễn ra cùng một lúc chứ không thực hiện một cách lần lượt.

Hầu hết các hàm số trên R đều được phát triển theo cơ chế lập trình vec-tơ. Nghĩa là các hàm số được dùng cho một biến kiểu số đều có thể áp dụng được cho một véc-tơ kiểu số hay các hàm số được dùng cho một biến kiểu chuỗi ký tự đều có thể áp dụng được cho một véc-tơ kiểu chuỗi ký tự. Trong ví dụ với cột (véc-tơ) \(created\_at\) của dữ liệu \(trump\_tweets\) ở trên, các hàm số được sử dụng như substr(), as.numeric(), … đều có đầu vào là một véc-tơ và trả lại giá trị là một véc-tơ có độ dài tương ứng.

Ngoài việc thực hiện tính toán trên các véc-tơ riêng lẻ, cơ chế hoạt động của R cũng cho phép thực hiện tính toán tương tác giữa các véc-tơ với nhau. Tương tác giữa hai hay nhiều véc-tơ với nhau luôn được thực hiện trên nguyên tắc các phần tử có cùng chỉ số của các véc-tơ sẽ tương tác với nhau. Thậm chí các véc-tơ tương tác với nhau có thể không có cùng kích thước mà vẫn cho kết quả. Chi tiết sẽ được thảo luận trong các phần tiếp theo.

1.3.2 Khởi tạo véc-tơ và các phép toán trên véc-tơ.

1.3.2.1 Khởi tạo véc-tơ.

Để khởi tạo một vec-tơ trong R bạn đọc có thể sử dụng bất kỳ một hàm số sẵn có với đầu ra là một véc-tơ với kiểu giá trị phù hợp. Hàm số thông dụng nhất được dùng để tạo véc-tơ trong R là hàm c(); \(c\) là viết tắt của concatenate, hoặc một vài tài liệu cho rằng \(c\) là viết tắt của combine. Về mặt ý nghĩa, hàm c() tập hợp các đối tượng được liệt kê trong dấu \(()\) lại để tạo thành một véc-tơ đối tượng duy nhất. Nếu các phần tử được liệt kê ra có cùng kiểu dữ liệu, đối tượng tượng tạo thành sẽ là một véc-tơ

x<-c(1,1,2,3,5,8,13,21) # x là một vec-tơ kiểu số
qua = c("chuối", "táo", "cam", "chanh") # qua là vec-tơ chứa tên các loại quả

Khi các biến được liệt kê bên trong hàm c() không cùng kiểu, R sẽ cố gắng phân tích các giá trị đó để đưa ra kết quả phù hợp. Nguyên tắc chung là nếu các giá trị được liệt kê bên trong hàm c() là kiểu số, kiểu logic, hoặc kiểu thời gian thì véc-tơ được tạo thành sẽ là véc-tơ kiểu số. Trong trường hợp có 1 biến được liệt kê ra là kiểu chuỗi ký tự, véc-tơ được tạo thành sẽ là véc-tơ kiểu chuỗi ký tự. Bạn đọc có thể kiểm tra giá trị của các véc-tơ sau:

x<-c(1,TRUE, FALSE) # Kết quả là một vec-tơ kiểu số
class(x)
## [1] "numeric"
x<-c(TRUE, as.Date("2023-12-31")) # Kết quả là một vec-tơ kiểu số
class(x)
## [1] "numeric"
x<-c(1, TRUE, as.Date("2023-12-31"),"MFE") # Kết quả là một vec-tơ kiểu chuỗi ký tự
class(x)
## [1] "character"

Các giá trị bên trong hàm c() cũng có thể là một véc-tơ khác, thậm chí có thể là một ma trận (matrix), hoặc là một đối tượng kiểu mảng (array). Giá trị đầu ra của hàm c() luôn luôn là một véc-tơ. Nếu là ma trận hoặc mảng hàm c() sẽ “duỗi” các phần tử ra thành 1 véc-tơ theo thứ tự các cột bắt đầu từ cột có chỉ số 1. Chúng ta sẽ quay lại vấn đề này khi thảo luận về ma trận và mảng.

x<-c(1, TRUE, as.Date("2023-12-31"),"MFE") # kết quả là một véc-tơ kiểu chuỗi ký tự
y<-c(x,"Actuary",x) # dùng véc-tơ x trong khai báo véc-tơ y

Bất kỳ hàm số sẵn có nào có đầu ra là véc-tơ đều có thể dùng để tạo thành véc-tơ. Các hàm mà chúng tôi hay sử dụng để khởi tạo véc-tơ trong R ngoài hàm c() còn có hàm rep() và hàm seq(). Hàm số rep(x,n) có ý nghĩa là lặp lại giá trị \(x\) (1 biến hoặc 1 véc-tơ) \(n\) lần. Hàm số seq(from = a, to = b,length = n) tạo thành một dãy số tăng dần (hoặc giảm dần) bắt đầu từ \(a\) kết thúc tại \(b\) và véc-tơ có độ dài là \(n\).

x<-rep(1,10^3) # Véc-tơ có các giá trị đều là 1, độ dài 1.000
y<-rep(c("a","b"),10^3) # Lặp lại véc-tơ ("a","b") 1.000 lần
z<-seq(from = 0,to = 1,length = 101) # Dãy số tăng dần từ 0 đến 1, độ dài là 101

Đầu ra của seq() luôn là một véc-tơ kiểu số. Nếu bạn đọc không sử dụng tùy biến \(length = n\), bạn đọc có thể sử dụng tùy biến là khoảng cách giữa hai số liên tiếp trong dãy số.

z1<-seq(from = 0,to = 1, 0.01) # dãy số tăng dần từ 0 đến 1, số sau lớn hơn số trước 0.01
z2<-seq(from = 1,to = 0, -0.01) # dãy số giảm dần từ 1 đến 0, số sau nhỏ hơn số trước 0.01

1.3.2.2 Các hàm số thường sử dụng trên véc-tơ

(#tab:unnamed-chunk-36)Các hàm thường sử dụng trên véc-tơ
Hàm số Ý nghĩa Áp dụng trên
length(x) Số lượng phần tử trong véc-tơ \(x\) Mọi kiểu véc-tơ
sum(x) Tổng các số trong véc-tơ \(x\) Kiểu số, logic, thời gian
prod(x) Tích các số trong véc-tơ \(x\) Kiểu số, logic, thời gian
mean() Giá trị trung bình của các số trong véc-tơ \(x\) Kiểu số, logic, thời gian
var(x) Phương sai của các giá trị trong véc-tơ \(x\) Kiểu số, logic, thời gian
sd(x) Độ lệch chuẩn của các giá trị trong véc-tơ \(x\) Kiểu số, logic, thời gian
min(x) Giá trị nhỏ nhất trong \(x\) Mọi kiểu véc-tơ
max(x) Giá trị lớn nhất trong \(x\) Mọi kiểu véc-tơ
quantile(x,p) Giá trị tại mức xác suất \(p\) của véc-tơ \(x\) Kiểu số, logic, thời gian
sort(x) Sắp xếp các phần tử của \(x\) theo thứ tự TĂNG dần Mọi kiểu véc-tơ
table(x) Cho biết tần suất xuất hiện của mỗi phần tử Mọi kiểu véc-tơ

Bạn đọc lưu ý rằng còn nhiều hàm số hữu ích khác được xây dựng sẵn khi tính toán với véc-tơ mà chúng tôi không liệt kê ở đây. Đồng thời, mỗi hàm số còn có các tùy biến đề sử dụng trong các hoàn cảnh khác nhau. Chẳng hạn khi trong véc-tơ \(x\) có giá trị \(NaN\) hoặc \(NA\) thì các hàm như \(sum(x)\), \(mean(x)\), … sẽ trả lại giá trị là \(NA\). Trong trường hợp này, bạn đọc cần sử dụng thêm tùy biến \(na.rm=TRUE\) để R hiểu rằng các phép tính toán chỉ thực hiện trên các giá trị có ý nghĩa.

x<-c(rep(1,10),2,3,NA)
sum(x) # sẽ trả lại giá trị là $NA$ vì trong $x$ có giá trị $NA$
## [1] NA
sum(x,na.rm=TRUE) # sẽ trả lại giá trị là $NA$ vì trong $x$ có giá trị $NA$
## [1] 15

Cách tốt nhất để hiểu và sử dụng hiệu quả và đúng mục đích các hàm số liệt kê ở trên là đọc hướng dẫn của hàm số đó. Trong cuốn sách này chúng tôi chỉ nhấn mạnh những ứng dụng mà chúng tôi cho rằng quan trọng khi ứng dụng các hàm số trong thực tế.

Các hàm số sử dụng trên các véc-tơ kiểu số như \(sum()\), \(mean()\), hay thậm chí cả \(var()\), \(sd()\) có thể hoạt động trên cả véc-tơ kiểu thời gian hoặc kiểu logic. Nếu phép toán thực hiện không thể giữ nguyên kiểu dữ liệu của véc-tơ thì R sẽ đổi véc-tơ kiểu thời gian hoặc logic sang kiểu số để thực hiện tính toán.

x<-c(as.Date("2023-01-01"),as.Date("2023-12-31"))
mean(x) # trả lại giá trị là kiểu thời gian
## [1] "2023-07-02"
sd(x) # kiểu thời gian ko có ý nghĩa nên R sẽ đổi x sang kiểu số để tính toán
## [1] 257.3869

Ngoài các nguyên tắc tính toán thông thường, bạn đọc thấy rằng R có thể sắp xếp các phần tử trong một véc-tơ bất kỳ bằng hàm sort() hoặc có thể lấy ra giá trị “lớn nhất” hoặc “nhỏ nhất” của một véc-tơ đó bằng hàm max() hoặc hàm min(). Điều này là khá hiển nhiên với các véc-tơ kiểu số. Trong trường hợp véc-tơ là véc-tơ kiểu logic hay kiểu ngày tháng, R sẽ đổi giá trị của véc-tơ đó sang kiểu số để tiến hành sắp xếp hay tìm ra giá trị lớn nhất, giá trị nhỏ nhất. Chắc hẳn bạn đọc sẽ đặt câu hỏi về cách sắp xếp các phần tử trong véc-tơ kiểu chuỗi ký tự. Đây là một vấn đề phức tạp liên quan đến việc mã hóa các ký tự trên máy tính và vượt quá phạm vi của cuốn sách. Bạn đọc chỉ cần ghi nhớ các nguyên tắc sau khi sắp xếp véc-tơ kiểu chuỗi ký tự:

  1. Nếu véc-tơ kiểu chuỗi ký tự được biến đổi thành kiểu factor thì thứ tự sắp xếp tăng dần sẽ phụ thuộc vào cách định nghĩa các mức độ (level) của véc-tơ kiểu factor.

  2. Khi so sánh hai chuỗi ký tự, phép so sánh sẽ được thực hiện ở ký tự thứ nhất trước, nếu hai ký tự đầu tiên giống nhau thì sẽ so sánh ký tự tiếp theo, và tiếp tục như thế đến khi có sự khác biệt.

  3. Các ký tự đặc biệt luôn được xếp trước (nhỏ hơn), sau đó đến các ký tự là các số, rồi đến chữ cái. Thứ tự sắp xếp của các ký tự số theo đúng thứ tự tăng dần từ 0 đến 9 trong khi thứ tự sắp xếp của các chữ cái là tăng dần theo bảng chữ cái. Chữ viết thường được viết trước (nhỏ hơn) chữ viết hoa của chữ cái đó. Chữ viết hoa của chữ cái đứng trước lại “nhỏ hơn” chữ viết thường của chữ đứng sau trong bảng chữ cái.

Trước khi sử dụng R để in ra kết quả, bạn đọc hãy thử “đoán” xem R sẽ trả lại kết quả như thế nào khi chạy các câu sắp xếp các véc-tơ sau theo thứ tự TĂNG dần:

sort(c("a","az","z")) # luôn sử dụng chữ cái đầu tiên để so sánh
## [1] "a"  "az" "z"
sort(c("a","az","z","A","Z")) # chữ cái đứng trước trong bảng chữ cái xếp trước
## [1] "a"  "A"  "az" "z"  "Z"
sort(c("a","az","z","A","Z","1a")) # số luôn đứng trước chữ cái
## [1] "1a" "a"  "A"  "az" "z"  "Z"
sort(c("a","az","z","A","Z","1a","@a")) # ký tự đặc biệt luôn đứng trước
## [1] "@a" "1a" "a"  "A"  "az" "z"  "Z"
sort(c("a","az","z","A","Z","1a","@a", "0123")) # ký tự đặc biệt < số < chữ cái
## [1] "@a"   "0123" "1a"   "a"    "A"    "az"   "z"    "Z"

Hàm sort() nếu không sử dụng thêm tham số sẽ luôn sắp xếp véc-tơ theo thứ tự tăng dần. Để sắp xếp véc-tơ theo thứ tự giảm dần, bạn đọc có thể sử dụng thêm tùy biến \(decreasing = TRUE\) hoặc ngắn gọn hơn là \(decreasing = T\) trong hàm sort().

sort(c(1,1,2,3,5,8,13,21), decreasing = TRUE) # sắp xếp theo thứ tự giảm dần
## [1] 21 13  8  5  3  2  1  1
sort(c("a","az","z","A","Z","1a","@a", "0123"),decreasing = T) # có thể thay TRUE bằng T
## [1] "Z"    "z"    "az"   "A"    "a"    "1a"   "0123" "@a"

1.3.2.3 Tính toán trên véc-tơ

Như đã đề cập ở phần trước, R là ngôn ngữ lập trình véc-tơ. Bạn đọc có thể sử dụng véc-tơ như một đối tượng trong các phép tính toán hoặc so sánh mà không cần phải tác động đến từng phần tử riêng lẻ của véc-tơ đó. Điều này là không thể thực hiện được với các ngôn ngữ lập trình cơ bản.

Trước hết, chúng ta có thể đưa một véc-tơ \(x\) kiểu số vào trong các phép tính toán thông thường như cộng, trừ, nhân, chia, lũy thừa, … với các số thực. Kết quả thu được sẽ là một véc-tơ có độ dài bằng với véc-tơ ban đầu:

x<-1:5 # tạo thành véc-tơ dãy số tự nhiên từ 1 đến 5
x * 2 # nhân véc-tơ với một số
## [1]  2  4  6  8 10
x ^ 2 # phép lũy thừa, đối tượng là
## [1]  1  4  9 16 25
x %% 2 # lấy phần dư trong phép chia cho 2
## [1] 1 0 1 0 1

Quan sát kết quả được in ra, bạn đọc có thể nhận thấy rằng nguyên tắc thực hiện phép tính véc-tơ \(x\) nhân với số 2, hay bất kỳ phép tính nào khác, là lấy các phần tử riêng lẻ trong véc-tơ \(x\) nhân lên 2 và lưu lại trong một véc-tơ mới. Tương tự như phép tính toán, phép so sánh cũng có thể thực hiện giữa một véc-tơ với biến riêng lẻ để cho kết quả là một véc-tơ của các biến logic.

x<-c(1,1,2,3,5,8,13,21) # véc-tơ x kiểu số
x == 1 # Trả lại giá trị TRUE tại các vị trí bằng 1.
## [1]  TRUE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE
(x > 10) | (x < 3) # trả lại giá trị TRUE tại các vị trí lớn hơn 10 hoặc nhỏ hơn 3
## [1]  TRUE  TRUE  TRUE FALSE FALSE FALSE  TRUE  TRUE
s<-c("a","az","z","A","Z","1a","@a", "0123")
s == "a" # Trả lại giá trị TRUE tại các vị trí bằng "a"
## [1]  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE

Hầu hết các hàm số sẵn có trong R, hoặc các hàm số được phát triển trong các thư viện của R, đều có thể áp dụng trên đối tượng là véc-tơ và nguyên tắc áp dụng hàm số trên véc-tơ cũng tương tự như nguyên tắc tính toán giữa véc-tơ với một số. Việc thực hiện tính toán sẽ được thực hiện trên các phần tử riêng lẻ của véc-tơ và sau đó lưu lại trong một véc-tơ mới có chiều dài bằng với véc-tơ ban đầu. Ví dụ như hàm nchar() cho biết một biến kiểu chuỗi ký tự có bao nhiêu ký tự. Khi sử dụng với một véc-tơ kiểu chuỗi ký tự sẽ trả lại giá trị là một véc-tơ kiểu số mà mỗi phần tử là số ký tự của phần tử tương ứng trong véc-tơ kiểu chuỗi ký tự

s<-c("a","az","z","A","Z","1a","@a", "0123")
nchar(s) # trả lại giá trị là một véc-tơ kiểu số
## [1] 1 2 1 1 1 2 2 4

Bằng cách kết hợp các hàm số trên véc-tơ và tương tác giữa véc-tơ với một biến, bạn đọc có thể tự tạo ra các hàm số, các phương pháp của riêng mình để giải quyết các vấn đề phức tạp hơn. Chẳng hạn như chúng ta muốn biết có bao nhiêu phần tử trong véc-tơ thỏa mãn một điều kiện nào đó, chúng ta có thể kết hợp hàm sum() với một biểu thức so sánh giữa véc-tơ với một số

x<-c(1,1,2,3,5,8,13,21) # véc-tơ x kiểu số
sum(x>10) # cho biết có bao nhiêu phần tử trong x lớn hơn 10
## [1] 2

Khi thực hiện phép so sánh \(x > 10\), do \(x\) là một véc-tơ kiểu số nên phép so sánh sẽ trả lại giá trị là \(TRUE\) tại các vị trí mà kết quả so sánh là đúng và \(FALSE\) tại các vị trí còn lại. Khi kết hợp với hàm sum(), các giá trị \(TRUE\) sẽ được đổi thành số 1 và \(FALSE\) được đổi thành 0. Kết quả thu được sẽ là số lượng các giá trị \(TRUE\) trong phép so sánh, hay nói một cách khác, là số các phần tử trong \(x\) thỏa mãn điều kiện lớn hơn \(10\). Tất nhiên với véc-tơ \(x\) có độ dài 10 như ở trên, bạn đọc có thể nhìn được một cách trực quan mà không cần hỗ trợ của R. Nhưng thực tế thì các véc-tơ mà chúng ta cần thực hiện tính toán sẽ có độ dài lớn hơn rất nhiều và bạn đọc không thể không dùng phần mềm hỗ trợ. Chẳng hạn như bạn đọc muốn biết có bao nhiêu câu tweets của cựu tổng thống Donald Trump có nhiều hơn 10.000 lượt yêu thích, bạn có thể kết hợp sum() với biểu thức so sánh. Véc-tơ chứa số lượt yêu thích với mỗi câu tweet là cột \(favorite\_count\) trong dữ liệu \(trump\_tweets\)

x<-trump_tweets$favorite_count # véc-tơ kiểu số cho biết mỗi câu được like bao nhiêu lần
sum(x>10^4) # cho biết có bao nhiêu phần tử trong x lớn hơn 10^4
## [1] 4958

Để biết tỷ lệ số câu tweet có số lượt yêu thích nhiều hơn 10.000, bạn đọc có thể kết hợp thêm với hàm length()

sum(x>10^4)/length(x) # cho biết có tỷ lệ phần tử trong x lớn hơn 10^4
## [1] 0.2388132

Có rất nhiều cách kết hợp các hàm số lại để đạt được kết quả mong muốn. Một kết quả phân tích có thể đạt được bằng các cách kết hợp khác nhau. Để sử dụng thành thạo chỉ có một cách duy nhất là bạn đọc hãy thực hành nhiều trên R và tự đúc kết kinh nghiệm của mình

1.3.3 Lấy véc-tơ con từ một véc-tơ

Khi làm việc với véc-tơ, chúng ta thường phải lấy các phần tử của véc-tơ ra theo một thứ tự hoặc lấy các phần tử con thỏa mãn các điều kiện nào đó và lưu kết quả vào một véc-tơ mới. Kỹ thuật này sẽ được thảo luận dưới đây.

1.3.3.1 Hai cách lấy véc-tơ con từ một véc-tơ

Để lấy một phần tử con của một véc-tơ \(x\) chúng ta sử dụng dấu ngoặc vuông \([]\). Chẳng hạn như để lấy ra phần tử thứ \(1\), chúng ta sử dụng \(x[1]\). Số 1 trong trường hợp này được gọi là chỉ số. Nhắc lại với bạn đọc rằng chỉ số của các phần tử trong véc-tơ của R là bắt đầu từ \(1\) và phần tử cuối cùng trong véc-tơ có chỉ số bằng với độ dài của véc-tơ đó. Nếu chúng ta sử dụng chỉ số lớn hơn độ dài của véc-tơ, R sẽ trả lại giá trị là \(NA\).

x<-c(1,1,2,3,5,8,13,21) # véc-tơ x kiểu số
x[1] # lấy ra phần tử thứ nhất trong x
## [1] 1
x[11] # độ dài của x là 10 nên giá trị trả lại sẽ là NA
## [1] NA

Bạn đọc có thể đặt câu hỏi là điều gì xảy ra nều sử dụng chỉ số \(0\) hoặc chỉ số là số âm. Hãy nói về chỉ số \(0\) trước. Khi gọi phần tử ở vị trí thứ 0 trong một véc-tơ bạn đọc sẽ nhận được một phần tử rỗng. Khái niệm rỗng có thể hiểu giống như khái niệm rỗng khi nói về một tập hợp không có phần tử. Tùy theo kiểu giá trị của véc-tơ ta sẽ có một phần tử rỗng với kiểu giá trị tương ứng

(#tab:unnamed-chunk-49)Giá trị tại chỉ số 0 của các kiểu véc-tơ
Kiểu véc-tơ Giá trị tại chỉ số 0
Kiểu số nguyên integer(0)
Kiểu số thực numeric(0)
Kiểu logical logical(0)
Kiểu chuỗi ký tự character(0)
Kiểu ngày tháng Date of length 0
Kiểu thời gian chính xác POSIXct of length 0

Khi sử dụng chỉ số âm đối với véc-tơ, R hiểu rằng chúng ta đang loại đi các phần tử. Thật vậy, \(x[-1]\) sẽ trả lại kết quả là một véc-tơ giống với véc-tơ \(x\) sau khi loại đi phần tử thứ nhất. Với số tự nhiên \(k, (k \in \mathbb{N}),\) \(x[-k]\) sẽ trả lại kết quả là véc-tơ \(x\) sau khi loại đi phần tử thứ \(k\). Nếu \(k\) lớn hơn độ dài của véc-tơ \(x\), véc-tơ nhận được sẽ đúng bằng \(x\). Sử dụng chỉ số âm cũng có thể hiểu là một cách để lấy một véc-tơ con từ một véc-tơ ban đầu. Đây là cách lấy véc-tơ con bằng cách sử dụng véc-tơ chỉ số kiểu số nguyên.

Có hai cách để lấy véc-tơ con từ một véc-tơ ban đầu, đó là

  1. Sử dụng một véc-tơ chỉ số kiểu số nguyên; và
  2. Sử dụng một véc-tơ chỉ số kiểu logic.

Từ véc-tơ \(x\) ban đầu, để lấy ra một véc-tơ con, trong trường hợp chúng ta đã biết chính xác các vị trí và thứ tự của các phần tử con mà chúng ta muốn lấy ra, chúng ta có thể lưu vị trí của các phần tử con này vào một véc-tơ khác tạm gọi là véc-tơ \(y\). Véc-tơ \(y\) còn được gọi là véc-tơ chỉ số. Sau đó, chúng ta chỉ cần sử dụng câu lệnh \(x[y]\) để lấy ra các phần tử của \(x\) tại các vị trí được lưu ở véc-tơ \(y\). Thật vậy, hãy thử quan sát ví dụ sau

x<-c("cam","táo","kiwi","chuối","nho") # véc-tơ x kiểu chuỗi ký tự
y<-c(3,5,2,3,1) # lấy ra véc-tơ con tại chỉ số y
x[y] # thứ thự trong véc-tơ con là x[3] -> x[5] -> x[2] -> x[3] -> x[1]
## [1] "kiwi" "nho"  "táo"  "kiwi" "cam"

Nếu trong véc-tơ chỉ số có giá trị lớn hơn độ dài của véc-tơ ban đầu, R sẽ trả lại giá trị là \(NA\) tại vị trí đó

x<-c("cam","táo","kiwi","chuối","nho") # véc-tơ x kiểu chuỗi ký tự
y<-c(3,5,2,10,3,1) # chỉ số 10 lớn hơn độ dài véc-tơ (5)
x[y] # vị trí thứ tư trong véc-tơ con sẽ là NA
## [1] "kiwi" "nho"  "táo"  NA     "kiwi" "cam"

Nếu chúng ta sử dụng véc-tơ chỉ số là số âm, R sẽ hiểu rằng chúng ta đang muốn loại đi một hay một số phần tử nào đó.

x<-c("cam","táo","kiwi","chuối","nho") # véc-tơ x kiểu chuỗi ký tự
y<-c(-3,-5,-2,-3) # véc-tơ chỉ số toàn số âm
x[y] # nhận được véc-tơ con sau khi loại đi các số thứ 2,3,5 trong y (chỉ còn x[1] rồi x[4])
## [1] "cam"   "chuối"

R sẽ báo lỗi nếu véc-tơ chỉ số \(y\) chứa cả số âm và số dương. Bạn đọc cần lưu ý vấn đề này. Trong thực tế, ít khi chúng ta biết chính xác vị trí mà chúng ta muốn lấy ra, hay nói cách khác chúng ta không thể trực tiếp khai báo giá trị vào véc-tơ chỉ số \(y\). Thông thường \(y\) sẽ là kết quả của các hàm số tạo chỉ số. Các hàm which() và hàm match() được thảo luận ở phần tiếp theo của cuốn sách là các phương pháp tuyệt vời để tạo ra các véc-tơ chỉ số kiểu số.

Phương pháp thứ hai để lấy một véc-tơ con từ véc-tơ \(x\) đó là sử dụng véc-tơ chỉ số kiểu logic. Cách lấy này sẽ rất thuận tiện khi bạn đọc muốn lấy ra một véc-tơ con của \(x\) bao gồm các phần tử thỏa mãn một điều kiện nào đó. Véc-tơ chỉ số, tạm gọi là véc-tơ \(y\), được tạo ra từ một phép so sánh, sau đó câu lệnh \(x[y]\) sẽ trả lại giá trị là một véc-tơ con của \(x\) bao gồm các phần tử mà vị trí tương ứng của nó trong véc-tơ \(y\)\(TRUE\). Lấy véc-tơ con bằng cách này, bạn đọc hãy luôn để độ dài của véc-tơ \(y\) bằng độ dài của véc-tơ \(x\). Khi độ dài của \(y\) không bằng độ dài của \(x\), câu lệnh \(x[y]\) vẫn trả lại kết quả, tuy nhiên hiểu được kết quả là khá phức tạp. Do đó chúng tôi khuyên bạn đọc hãy luôn đảm bảo rằng véc-tơ chỉ số kiểu logic và véc-tơ ban đầu luôn có cùng độ dài.

Giả sử với véc-tơ \(x\) chứa tên các loại quả, chúng ta muốn lấy ra tên các loại quả có tên dài hơn 3 ký tự. Chúng ta không biết chính xác các quả này nằm ở vị trí nào trong \(x\) nên không thể tạo véc-tơ chỉ số kiểu số. Trong trường hợp này, chúng ta sẽ tạo một véc-tơ chỉ số \(y\) kiểu logic như sau

x<-c("cam","táo","kiwi","chuối","nho") # véc-tơ x kiểu chuỗi ký tự
y<-(nchar(x)>3) # y có độ dài bằng x, giá trị TRUE tại vị trí có độ dài > 3
y # hiển thị giá trị của y
## [1] FALSE FALSE  TRUE  TRUE FALSE
x[y] # trả lại giá trị trong x mà vị trí tương ứng trong y là TRUE
## [1] "kiwi"  "chuối"

Đây là cách lấy ra các véc-tơ con rất hiệu quả khi làm việc với dữ liệu. Các cột dữ liệu là các véc-tơ có cùng độ dài, do đó chỉ số \(y\) có thể được tạo thành từ phép so sánh một cột dữ liệu và véc-tơ \(x\) lại là một cột dữ liệu khác. Chẳng hạn như chúng ta muốn lấy ra các câu tweet của cựu tổng thống Donald Trump được like nhiều hơn 10.000 lần và lưu vào một véc-tơ, chúng ta chỉ cần thực hiện như sau:

x<-trump_tweets$text # véc-tơ x chứa tất cả các câu tweet
y<-trump_tweets$favorite_count > 10^4 # y là chỉ số, nhận giá trị TRUE tại các câu nhiều hơn 10.000 like
z<-x[y] # z chỉ chứa các câu tweet nhiều hơn 10.000 like

Điều gì xảy ra nếu độ dài của \(y\) không giống như độ dài của \(x\). Trong trường hợp \(y\) có độ dài nhỏ hơn độ dài của \(x\), R sẽ tạo ra một véc-tơ \(y1\) có độ dài bằng với độ dài của \(y\) bằng cách lặp lại giá trị của \(y\) cho đến khi véc-tơ thu được có độ dài bằng \(x\). Hãy quan sát ví dụ sau

x<-c("cam","táo","kiwi","chuối","nho") # véc-tơ x kiểu chuỗi ký tự độ dài 5
y<-c(TRUE,FALSE) # y có độ dài là 2, nhỏ hơn 5
x[y] # là véc-tơ có độ dài 5
## [1] "cam"  "kiwi" "nho"

Kết quả thu được tương tự như khi chúng ta thực hiện phép lấy véc-tơ con thông qua một véc-tơ chỉ số \(y1\) có độ dài bằng 5 như sau

y1<-rep(y,3) # lặp lại y cho đến khi có độ dài lớn hơn x (độ dài của y1 là 6 > 5)
y1<-y1[1:length(x)] # chỉ số y1 là chỉ lấy đến đúng độ dài của x
x[y1] # cho kết quả giống như khi viết x[y]
## [1] "cam"  "kiwi" "nho"

Nếu độ dài của véc-tơ chỉ số \(y\) lớn hơn độ dài của \(x\), tại các vị trí của \(y\) mà chỉ số vẫn nhỏ hơn hoặc bằng chiều dài của \(x\), việc lấy ra phần tử con vẫn theo quy tắc thông thường, nghĩa là lấy ra các phần tử tương ứng với giá trị \(TRUE\) và bỏ qua các phần tử tương ứng với giá trị \(FALSE\). Tại các vị trí của \(y\) mà chỉ số lớn hơn chiều dài của \(x\), R sẽ bỏ qua các phần tử có giá trị là \(FALSE\) và sẽ trả lại giá trị là \(NA\) mỗi khi gặp giá trị \(TRUE\). Bạn đọc có thể quan sát ví dụ sau

x<-c("cam","táo","kiwi","chuối","nho") # véc-tơ x kiểu chuỗi ký tự độ dài 5
y<-c(nchar(x)>3,FALSE,TRUE) # y có độ dài là 7, vị trí thứ 6 là FALSE, thứ 7 là TRUE
x[y] # x sẽ là các loại quả có tên dài hơn 3 ký tự, theo sau là NA do y[7] là TRUE
## [1] "kiwi"  "chuối" NA

Do sự phức tạp khi tương tác giữa các véc-tơ có không cùng độ dài nên chúng tôi khuyên bạn đọc hãy luôn luôn thực hiện các phép tính toán với các véc-tơ có cùng độ dài để kiểm soát được kết quả khi làm việc với R. Trong phần tiếp theo chúng ta sẽ thảo luận về các hàm số để tạo ra véc-tơ chỉ số.

1.3.4 Các hàm tạo chỉ số trong véc-tơ

Có một nhóm các hàm số thường được sử dụng khi làm việc với chỉ số của các phần tử trong véc-tơ. Các hàm số này có thể được phỏng theo bằng cách kết hợp một vài kỹ thuật chỉ số đã đề cập đến ở chương trước. Tuy nhiên chúng tôi khuyên bạn đọc nên sử dụng các hàm có sẵn được trình bày trong phần này bởi sự tiện lợi và sự dễ hiểu của các dòng lệnh. Các hàm số liên quan đến chỉ số của véc-tơ được liệt kê trong bảng sau

(#tab:unnamed-chunk-59)Các hàm số liên quan đến chỉ số của véc-tơ
Hàm số Ý nghĩa
which() Chỉ số của các phần tử nhận giá trị là TRUE của một véc-tơ kiểu logical
match() Cho biết chỉ số của một phần tử nằm trong một véc-tơ khác
%in% Trả lại giá trị là TRUE nếu một phần tử của một véc-tơ có nằm trong một véc-tơ khác
rank Trả lại giá trị là thứ tự của phần tử khi xếp véc-tơ theo thứ tự TĂNG dần
order() Trả lại giá trị là chỉ số của các phần tử sau khi xếp theo thứ tự TĂNG dần

1.3.4.1 Hàm which()

Hàm which() áp dụng trên một véc-tơ kiểu logic và cho biết các vị trí nào trong véc-tơ logic có giá trị là \(TRUE\). Có hai biến thể của hàm which() thường được sử dụng là which.min()which.max() cho biết chỉ số (vị trí) của giá trị lớn nhất và chỉ số của giá trị nhỏ nhất.

x<-c(20,40,60,50,30,10) # Véc-tơ kiểu số
which(x>40) # Các chỉ số (vị trí) trong véc-tơ x có giá trị > 40
## [1] 3 4
which.min(x) # Số nhỏ nhất trong x (số 10) nằm ở vị trí nào
## [1] 6
which.max(x) # Số lớn nhất trong x (số 60) nằm ở vị trí nào 
## [1] 3

Trong trường hợp \(x\) có nhiều giá trị bằng với giá trị lớn nhất hoặc nhiều giá trị bằng với giá trị nhỏ nhất, các hàm which.min()which.max() luôn luôn trả lại giá trị là chỉ số nhỏ hơn.

x<-c(20,40,60,50,30,10,60,10) # Véc-tơ kiểu số
which.min(x) # Số nhỏ nhất trong x (số 10) nằm ở vị trí nào
## [1] 6
which.max(x) # Số lớn nhất trong x (số 60) nằm ở vị trí nào 
## [1] 3

Bạn đọc sử dụng hàm which() để tạo ra véc-tơ chỉ số khi muốn lấy ra các phần tử của một véc-tơ thỏa mãn một điều kiện nào đó. Ví dụ như chúng ta muốn lấy ra các các câu tweet của Donald Trum có nhiểu hơn 10.000 lượt yêu thích bằng một véc-tơ chỉ số:

x<-trump_tweets$text # Véc-tơ chứa tất cả các câu tweet
y<-which(trump_tweets$favorite_count>10^4) # Véc-tơ kiểu số cho biết các chỉ số (vị trí) nào có nhiều hơn 10.000 lượt thích
z<-x[y] # z chứa tất cả các câu tweet có nhiều hơn 10.000 like

1.3.4.2 Hàm match() và toán tử %in\%

Hàm \(match()\) là hàm số cho phép tương tác giữa hai véc-tơ có độ dài khác nhau. Cho \(x\)\(y\) là hai véc-tơ có cùng kiểu, câu lệnh match(y,x) sẽ trả lại giá trị là một véc-tơ, tạm gọi là \(z\), có độ dài bằng với độ dài của véc-tơ \(y\), đồng thời \(z[1]\) cho biết \(y[1]\) có chỉ số (nằm ở vị trí) nào trong véc-tơ \(x\); \(z[2]\) cho biết \(y[2]\) có chỉ số (nằm ở vị trí) nào trong véc-tơ \(x\),… Các phần tử của \(y\) không xuất hiện trong \(x\) sẽ cho giá trị tương ứng trong \(z\)\(NA\).

x<-c(20,40,60,50,30,10) # Véc-tơ x kiểu số
y<-c(60,10,70) # véc-tơ y kiểu số
match(y,x) # cho biết từng phần tử của y nằm ở vị trí thứ bao nhiêu trong x
## [1]  3  6 NA

Chúng ta có thể thấy rằng giá trị 70 không xuất hiện trong \(x\) nên giá trị thứ 3 trong véc-tơ kết quả là \(NA\). Lưu ý rằng hàm match() luôn luôn tìm đến chỉ số đầu tiên trong véc-tơ \(x\) có giá trị khớp với giá trị của véc-tơ \(y\), nghĩa là trong \(x\) có nhiều hơn một giá trị khớp với giá trị của \(y\), hàm match() cho kết quả là chỉ số nhỏ hơn. Bạn đọc quán sát ví dụ dưới dây khi véc-tơ \(x\) có nhiều giá trị khớp với giá trị của \(y\):

x<-c(20,40,60,50,30,10,20,10) # véc-tơ x kiểu số, giá trị 10 và 20 xuất hiện nhiều lần
y<-c(10,20) # véc-tơ y kiểu số
match(y,x)
## [1] 6 1

Các giá trị 10 và 20 của \(y\) xuất hiện hai lần trong \(x\), tuy nhiên hàm match() sẽ trả lại giá trị là 6 và 1 bởi vì số 10 xuất hiện lần đầu tiên ở vị trí thứ 6 trong \(x\) và số 20 xuất hiện lần đầu tiên ở vị trí thứ 1 trong \(x\).

Hàm match() trả lại kết quả là véc-tơ chỉ số nên sẽ phù hợp với việc lấy véc-tơ con theo chỉ số kiểu số. Một phương pháp khác để làm việc với chỉ số của véc-tơ là toán tử %in%. Toán tử %in% được sử dụng để cho biết mỗi phần tử của một véc-tơ có nằm trong một véc-tơ khác hay không. Câu lệnh y %in% x sẽ trả lại giá trị là một véc-tơ kiểu logic \(z\) có độ dài bằng với độ dài của \(y\), \(z[i]\) nhận giá trị là \(TRUE\) nếu \(y[i]\) có xuất hiện trong \(x\) và nhận giá trị là \(FALSE\) nếu \(y[i]\) không xuất hiện trong \(x\).

x<-c(20,40,60,50,30,10) # Véc-tơ x kiểu số
y<-c(60,10,70) # véc-tơ y kiểu số
y %in% x # cho biết từng phần tử của y có nằm trong x hay không
## [1]  TRUE  TRUE FALSE

Hình vẽ dưới đây minh họa kết quả được trả ra của hàm match() và toán tử \%in\%

Hàm match() và toán tử \%in\% cho phép tương tác giữa các véc-tơ có độ dài khác nhau nên rất hiệu quả khi bạn đọc muốn kết nối nhiều dữ liệu khác nhau. Bạn đọc hãy đọc ví dụ dưới đây để hình dung cách sử dụng hàm match() khi kết nối hai dữ liệu.

Giả sử chúng ta có danh sách điểm học tại trường đại học của ba sinh viên ngành actuary có mã sinh viên lần lượt là “MSV001”, “MSV002”, “MSV003” khi học các môn học “Xác suất”, “Toán tài chính”, và “Đầu tư và thị trường tài chính”. Thông tin được lưu trong một dữ liệu tên là “diem_hoc_DH”. Sinh viên ngành actuary ngoài các môn học ở trường đại học có thể thi các môn học tại các hiệp hội nghề nghiệp actuary để lấy chứng chỉ hành nghề. Thông tin về điểm thi chứng chỉ được lưu trong dữ liệu có tên là “diem_chung_chi_Actuary”. Khi xét tốt nghiệp, sinh viên có quyền lấy điểm thi chứng chỉ tại các hiệp hội để thay thế cho điểm học tại trường đại học của môn học tương ứng nếu điểm thi chứng chỉ cao hơn. Dữ liệu về điểm thi tại trường đại học và thi chứng chỉ hành nghề như sau:

(#tab:unnamed-chunk-68)Điểm thi tại trường đại học (diem_hoc_DH)
Mã sinh viên Môn học Điểm thi
MSV001 Xác suất 5
MSV002 Xác suất 7
MSV003 Xác suất 9
MSV001 Toán tài chính 10
MSV002 Toán tài chính 6
MSV003 Toán tài chính 8
MSV001 Đầu tư và thị trường tài chính 9
MSV002 Đầu tư và thị trường tài chính 5
MSV003 Đầu tư và thị trường tài chính 10
(#tab:unnamed-chunk-70)Điểm thi chứng chỉ (diem_chung_chi_Actuary)
Mã sinh viên Môn học Điểm thi
MSV005 Xác suất 8
MSV002 Xác suất 9
MSV004 Xác suất 10
MSV003 Toán tài chính 10
MSV002 Toán tài chính 9
MSV001 Đầu tư và thị trường tài chính 8

Để tìm được điểm thi chứng chỉ của viên trong bảng “diem_hoc_DH” chúng ta phải kết nối (sử dụng hàm match()) bảng này với bảng “diem_chung_chi_Actuary” thông qua mã sinh viên và tên môn học. Việc kết nối sẽ được thực hiện bằng cách tạo ra trên mỗi bảng một véc-tơ có gọi tên là \(key\) là tổ hợp của mã sinh viên và tên môn học.

Trước hết bạn đọc có thể tạo hai dữ liệu trên như sau:

# du lieu diem_hoc_DH
MSV <- rep(c( "MSV001", "MSV002", "MSV003"),3)
Mon_hoc <- c(rep("Xác suất",3),rep("Toán tài chính",3),rep("Đầu tư và thị trường tài chính",3))
Diem <- c(5,7,9,10,6,8,9,5,10)
diem_hoc_DH <- data.frame(MSV, Mon_hoc, Diem)

# du lieu diem_chung_chi_Actuary
MSV <- c("MSV005", "MSV002", "MSV004", "MSV003", "MSV002", "MSV001")
Mon_hoc <- c("Xác suất", "Xác suất", "Xác suất", "Toán tài chính", "Toán tài chính", "Đầu tư và thị trường tài chính")
Diem <- c(8,9,10,10,9,8)
diem_chung_chi_Actuary <- data.frame(MSV, Mon_hoc, Diem)

Chúng ta tạo ra hai véc-tơ để kết nối hai bảng, véc-tơ tạo ra bằng cách kết hợp từ véc-tơ chứa mã sinh viên và véc-tơ tên môn học

diem_hoc_DH_key<- paste(diem_hoc_DH$MSV, diem_hoc_DH$Mon_hoc)
diem_chung_chi_Actuary_key<-paste(diem_chung_chi_Actuary$MSV, diem_chung_chi_Actuary$Mon_hoc)

Toán tử \%in\% sẽ cho chúng ta biết những phần tử nào trong \(diem\_hoc\_DH\_key\) nằm trong \(diem\_chung\_chi\_Actuary\_key\), hay nói một cách khác, sinh viên nào trong bảng “diem_hoc_DH” có thi chứng chỉ tương ứng với môn học ở trường đại học:

y<-diem_hoc_DH_key %in% diem_chung_chi_Actuary_key

Chỉ số \(y\) là kết quả của toán tử \%in\% nên sẽ có dạng logical. \(y\) có độ dài là 9 bằng với số dòng của dữ liệu \(diem\_hoc\_DH\) và cho biết tương ứng mỗi sinh viên có thi chứng chỉ môn học tương ứng hay không. Chẳng hạn như muốn tạo ra danh sách thi chứng chỉ của sinh viên lớp Actuary 60:

data.frame(MSV = diem_hoc_DH$MSV[y], # Lọc véc-tơ cột MSV bằng véc-tơ kiểu logic y 
           Diem = diem_hoc_DH$Mon_hoc[y]) # Lọc véc-tơ cột tên môn học bằng véc-tơ kiểu logic y 
##      MSV                           Diem
## 1 MSV002                       Xác suất
## 2 MSV002                 Toán tài chính
## 3 MSV003                 Toán tài chính
## 4 MSV001 Đầu tư và thị trường tài chính

Để tìm được điểm thi chứng chỉ của các sinh viên lớp Actuary 60 chúng ta cần biết kết nối mã sinh viên và môn học từ bảng \(diem\_hoc\_DH\) đến bảng \(diem\_chung\_chi\_Actuary\) bằng cách sử dụng hàm match()

y<-match(diem_hoc_DH_key,diem_chung_chi_Actuary_key)

Véc-tơ \(y\) có độ dài bằng 9, cho biết mỗi dòng của dữ liệu \(diem\_hoc\_DH\) tương ứng với dòng thứ bao nhiêu (chỉ số) của dữ liệu \(diem\_chung\_chi\_Actuary\). Giá trị \(NA\) trong \(y\) có ý nghĩa là dòng tương ứng của dữ liệu \(diem\_hoc\_DH\) không xuất hiện trong \(diem\_chung\_chi\_Actuary\) (sinh viên không thi chứng chỉ môn học tương ứng). Chúng ta có thể thêm một cột (véc-tơ) gọi là \(diem\_CT\) cho bảng \(diem\_hoc\_DH\)

diem_hoc_DH$diem_CT<-diem_chung_chi_Actuary$Diem[y] # lấy véc-tơ con bẳng chỉ số kiểu số

Như vậy chúng ta đã có một dữ liệu với điểm học trên lớp và điểm thi chứng chỉ của các sinh viên

(#tab:unnamed-chunk-77)Điểm thi và điêm chứng chỉ
Mã sinh viên Môn học Điểm thi Điểm chứng chỉ
MSV001 Xác suất 5 NA
MSV002 Xác suất 7 9
MSV003 Xác suất 9 NA
MSV001 Toán tài chính 10 NA
MSV002 Toán tài chính 6 9
MSV003 Toán tài chính 8 10
MSV001 Đầu tư và thị trường tài chính 9 8
MSV002 Đầu tư và thị trường tài chính 5 NA
MSV003 Đầu tư và thị trường tài chính 10 NA

1.3.4.3 Hàm rank() và hàm order().

Hàm rank(x) trả lại giá trị là thứ tự (rank) của một phần tử trong véc-tơ \(x\) khi sắp xếp \(x\) theo thứ tự tăng dần. Thứ tự tăng dần ở đây được sử dụng đối với các véc-tơ kiểu chuỗi ký tự.

x<-c(20,40,60,50,30,10) # Véc-tơ x kiểu số
rank(x) # tương ứng với số lớn nhất (60) là chỉ số 6, tương ứng với 10 là chỉ số 1
## [1] 2 4 6 5 3 1

Lưu ý rằng hàm rank() có một tùy chọn quan trọng là \(ties.method\). Khi bạn đọc không sử dụng tùy chọn này, giá trị mặc định là \("average"\). Tùy chọn \(ties.method\) chỉ có ý nghĩa khi \(x\) có các giá trị giống nhau. Trong trường hợp tất cả các phần tử trong \(x\) là đôi một khác nhau, bất kỳ tùy chọn nào đối với \(ties.method\) cũng trả lại một kết quả duy nhất.

Khi \(x\) có giá trị bị lặp lại, bạn đọc hãy quan sát ví dụ sau để thấy sự khác biệt khi sử dụng tùy chọn \(ties.method\)

x<-c(10,10,10,20,20) # Véc-tơ x kiểu số
rank(x,ties.method = "first") # Trong các giá trị bằng nhau, giá trị xuất hiện TRƯỚC có rank nhỏ hơn
## [1] 1 2 3 4 5
rank(x,ties.method = "last") # Trong các giá trị bằng nhau, giá trị xuất hiện SAU có rank nhỏ hơn
## [1] 3 2 1 5 4
rank(x,ties.method = "min") # Các giá trị bằng nhau có rank giống nhau và bằng rank nhỏ nhất
## [1] 1 1 1 4 4
rank(x,ties.method = "max") # Các giá trị bằng nhau có rank giống nhau và bằng rank lớn nhất
## [1] 3 3 3 5 5
rank(x,ties.method = "average") # Các giá trị bằng nhau có rank bằng nhau và bằng rank trung bình
## [1] 2.0 2.0 2.0 4.5 4.5
rank(x,ties.method = "random") # Các giá trị bằng nhau có rank bằng nhau và bằng rank trung bình
## [1] 3 2 1 5 4
  • Khi \(ties.method\) nhận giá trị là \("first"\), giá trị trả lại là \(1, 2, 3, 4, 5\). Ba số 10 liền nhau ở phần đầu của véc-tơ \(x\) được xếp thứ tự theo nguyên tắc số nào xuất hiện trước là có thứ tự NHỎ hơn, do đó thứ tự của ba số 10 này trong véc-tơ \(x\) khi xếp \(x\) theo thứ tự tăng dần là \(1 \rightarrow 2 \rightarrow 3\). Tương tự với hai số 20 ở cuối vec-tớ \(x\), số 20 xuất hiện trước được hiểu là có thứ tự trước số 20 xuất hiện sau, do đó thứ tự của hai số 20 sẽ là \(4 \rightarrow 5\)

  • Khi \(ties.method\) nhận giá trị là \("last"\), giá trị trả lại là \(3, 2, 1, 5, 4\). Ba số 10 liền nhau ở phần đầu của véc-tơ \(x\) được xếp thứ tự theo nguyên tắc số nào xuất hiện trước là có thử tự LỚN hơn, do đó thứ tự của ba số 10 này trong véc-tơ \(x\) khi xếp \(x\) theo thứ tự tăng dần là \(3 \rightarrow 2 \rightarrow 1\). Tương tự với hai số 20 ở cuối vec-tớ \(x\), số 20 xuất hiện trước được hiểu là có thứ tự LỚN hơn số 20 xuất hiện sau, do đó thứ tự của hai số 20 sẽ là \(5 \rightarrow 4\)

  • Khi \(ties.method\) nhận giá trị là \("min"\), giá trị trả lại là \(1, 1, 1, 4, 4\). Ba số 10 liền nhau ở phần đầu của véc-tơ \(x\) có thứ tự bằng nhau là 1. Đây chính là thứ tự nhỏ nhất của ba số khi xếp các số này theo tùy chọn \(ties.method = "first"\) (thứ tự của 3 số khi \(ties.method = "first"\) là 1, 2, 3). Tương tự ta có thứ tự của hai số 20 tiếp theo bằng nhau và bằng 4 (là giá trị nhỏ nhất trong (4,5)).

  • Tùy chọn \("max"\) ngược lại với \("min"\). Thứ tự của ba số 10 đầu tiên trong \(x\) đều bằng 3 - là số lớn nhất trong (1, 2, 3) đồng thời thứ tự của hai số 20 tiếp theo đều là 5 - là số lớn nhất trong (4,5).

  • Khi \(ties.method\) nhận giá trị là \("average"\), cũng là giá trị mặc định khi sử dụng hàm \(rank()\), thứ tự của ba số 10 ở đầu véc-tơ \(x\) được tính là trung bình của thứ tự khi xếp theo tùy chọn \("first"\). Thật vậy, thứ tư của ba số khi \(ties.method\) nhận giá trị là \("first"\)\(1 \rightarrow 2 \rightarrow 3\). Thứ tự khi \(ties.method\) nhận giá trị là \("average"\)\[ \cfrac{1 + 2 + 3}{3} = 2 \] và thứ tự của hai số 20 ở cuối véc-tơ là \[ \cfrac{4 + 5}{2} = 4.5 \]

  • Cuối cùng, khi \(ties.method\) nhận giá trị là \("random"\), thứ tự của ba số 10 ở đầu véc-tơ \(x\) là một \(hoán\) \(vị\) \(ngẫu\) \(nhiên\) của (1,2,3) - thứ tự của ba số khi \(ties.method\) nhận giá trị là \("first"\). Bạn đọc có thể thấy rằng hai lần gọi hàm \(rank()\) với tùy chọn \(ties.method = "random"\) có thể cho kết quả là khác nhau.

Một hàm số khác trả lại giá trị là chỉ số của véc-tơ là hàm order(). Câu lệnh y<-order(x) trả lại giá trị cho véc-tơ \(y\) là các chỉ số của \(x\) sao cho:

  • \(y[1]\) là chỉ số của số nhỏ nhất trong véc-tơ \(x\);

  • \(y[2]\) là chỉ số của số nhỏ thứ hai trong véc-tơ \(x\); …

  • số cuối cùng trong véc-tơ \(y\) là chỉ số của số lớn nhất trong véc-tơ \(x\).

Khi muốn lấy chỉ số của véc-tơ \(x\) nhưng theo thứ tự giảm dần bạn đọc sử dụng tùy biến \(decreasing = TRUE\) trong hàm order(). Khái niệm tăng dần và giảm dần cũng có thể hiểu cho các véc-tơ kiểu thời gian, kiểu factor hay kiểu chuỗi ký tự.

x<-c(20,40,60,50,30,10) # Véc-tơ kiểu số
order(x) # chỉ số khi xếp x theo thứ tự TĂNG dần
## [1] 6 1 5 2 4 3
order(x, decreasing = TRUE) # chỉ số khi xếp x theo thứ tự GIẢM dần
## [1] 3 4 2 5 1 6

Hàm order(x) cho kết quả là 6 tại vị trí thứ nhất có nghĩa là số nhỏ nhất trong \(x\) nằm ở vị trí thứ sáu trong véc-tơ này (số 10). Vị trí thứ hai trong order(x) nhận giá trị là 1 có nghĩa là số nhỏ thứ hai trong \(x\) nằm ở vị trí thứ nhất trong véc-tơ này, và cứ tiếp tục như thế. Vị trí cuối cùng trong order(x) có giá trị là 3 có nghĩa là số lớn nhất trong véc-tơ \(x\) nằm ở vị trí thứ 3 trong véc-tơ này.

Hàm order(x) có thể được phỏng theo được bằng cách khớp chỉ số của véc-tơ \(x\) với hàm rank(x, ties.method = "first"), thật vậy:

x<-c(20,20,10,10,10) # véc-tơ kiểu số có các giá trị giống nhau
chiso<-1:length(x) # chỉ số tăng dần từ 1 đến độ dài của x
match(chiso,rank(x, ties.method = "first")) # match chiso với rank
## [1] 3 4 5 1 2
order(x) # cho kết quả giống như ở trên
## [1] 3 4 5 1 2

Sử dụng hàm order() bạn đọc có thể dễ dàng lấy ra các giá trị nhỏ (hoặc lớn) thứ \(k\) trong một véc-tơ. Chẳng hạn như bạn đọc muốn lấy ra câu tweet có sốt lượt yêu thích nhiều thứ hai của cựu tổng thống Donald Trump từ dữ liệu \(trump\_tweet\), bạn có thể sử dụng hàm order() như sau

y<-order(trump_tweets$favorite_count, decreasing = T)[2] # vị trí của câu tweet được like nhiều thứ 2
trump_tweets$text[y] # lấy ra câu tweet được like nhiều thứ hai
## [1] "Why would Kim Jong-un insult me by calling me \"old,\" when I would NEVER call him \"short and fat?\" Oh well, I try so hard to be his friend - and maybe someday that will happen!"

1.4 Lập trình R

Để viết các chương trình phức tạp hơn trong R, bạn đọc sẽ cần kiểm soát tốt trình tự mà các dòng lệnh của mình. Một cách cơ bản để làm được việc này là thực hiện một số câu lệnh nhất định phụ thuộc vào một hoặc một số điều kiện hay còn gọi là viết các câu lệnh rẽ nhánh. Một cách kiểm soát khác là sử dụng vòng lặp nhằm lặp lại một nhóm các câu lệnh một số lần nhất định. Trong phần này, chúng ta sẽ khám phá những kiến thức lập trình cơ bản này trong ngôn ngữ lập trình R. Các kiến thức về lập trình bao gồm có cách sử dụng câu lệnh rẽ nhánh (if-else), cách sử dụng vòng lặp (for, while, và repeat) và một vài cấu trúc khác giúp bạn đọc điều khiển được cách thực hiện các dòng lệnh của mình.

1.4.1 Câu lệnh điều kiện

1.4.1.1 Câu lệnh \(if\)\(if-else\)

Bạn đọc sử dụng câu lệnh điều kiệu để thông báo cho R biết một câu lệnh, hay một nhóm câu lệnh chỉ thực hiện khi một điều kiện nào đó được thực thi. Dưới đây là cách viết của câu lệnh if trong ngôn ngữ R

if ("Biểu thức điều kiện"){ 
  "Nhóm các câu lệnh thực hiện khi biểu thức điều kiện là ĐÚNG"
}

Bạn đọc có thể thực hiện một đoạn lệnh có biểu thức điều kiện cụ thể như sau

x<-1; y<-2 # Dòng lệnh 1: tạo biến x có giá trị là 1 và biến y có giá trị là 2
if (x<10){ # Dòng lệnh 2: Nếu x nhỏ hơn 10 thì thực hiện các câu lệnh nằm trong {}
  y<-4 # Dòng lệnh 3: Thay đổi, gán giá trị y bằng 4
} # Dòng lệnh 4: kết thúc câu lệnh if

Khi thực hiện nhóm các câu lệnh ở trên, dòng lệnh thứ 3 chỉ được thực hiện nếu biểu thức điều kiện được viết trong dấu ngoặc () ở dòng lệnh thứ 2 nhận giá trị là TRUE. Nếu biểu thức điều kiện đó nhận giá trị là FALSE, R sẽ không thực hiện các dòng lệnh số 3. Sau khi R thực thi các dòng lệnh 1, biến \(x\) nhận giá trị là 1 nên phép so sánh \(x<10\) sẽ cho kết quả là \(TRUE\). Do đó, dòng lệnh 3 gán giá trị mới bằng 4 cho biến \(y\) sẽ được thực hiện. Bạn đọc có thể kiểm tra được rằng sau khi thực hiện đoạn lệnh ở trên, giá trị của biến \(y\) sẽ bằng 4 chứ không phải là 2 như khởi tạo ở dòng lệnh số 1.

Khi sử dụng câu lệnh điều kiện if, sẽ không có câu lệnh nào được thực hiện trong trường hợp biểu thức điều kiện nhận giá trị là sai. Trong thực tế, đa phần các đoạn lệnh rẽ nhánh sẽ có các câu lệnh phải thực thi khi biểu thức điều kiện nhận giá trị là sai. Để thực hiện được việc này, bạn đọc sử dụng câu lệnh if kết hợp với else như sau

if ("Biểu thức điều kiện"){ 
  "Nhóm các câu lệnh thực hiện khi biểu thức điều kiện là ĐÚNG"
} else {
  "Nhóm các câu lệnh thực hiện khi biểu thức điều kiện là SAI"
}

Bạn đọc có thể quan sát sự thay đổi giá trị của biến \(y\) sau khi thực hiện đoạn lệnh như sau

x<-1; y<-2 # Dòng lệnh 1: tạo biến x có giá trị là 1 và biến y có giá trị là 2
if (x==10){ # Dòng lệnh 2: Nếu x bằng 10 thì thực hiện các câu lệnh nằm trong {} của if
  y<-4 # Dòng lệnh 3: Thay đổi, gán giá trị y bằng 4
} else { # Dòng lệnh 4: Nếu x KHÁC 10 thì thực hiện các câu lệnh nằm trong {} của else
  y<-8 # Dòng lệnh 5: Thay đổi, gán giá trị y bằng 4
} # Dòng lệnh 6: kết thúc câu lệnh if-else

Do biểu thức điều kiện \(x==10\) nhận giá trị là \(FALSE\) nên R sẽ không thực hiện dòng lệnh số 3 mà chuyển qua thực hiện dòng lệnh số 5. Giá trị của \(y\) sau khi thực hiện đoạn lệnh ở trên sẽ là 8. Nếu trong dòng lệnh 1, bạn đọc sửa giá trị của \(x\) thành 10 thay vì 1, dòng lệnh 3 sẽ được thực hiện và dòng lệnh số 5 không được thực hiện do đó giá trị của \(y\) sau khi thực hiện đoạn lệnh lúc này sẽ là 4.

Biểu thức điều kiện trong câu lệnh if phải là một biến kiểu logic. Nếu do sơ ý, biểu thức điều kiện là một véc-tơ của các biến kiểu logic, câu lệnh if sẽ chỉ tính đến giá trị đầu tiên trong véc-tơ.

dieukien<-c(TRUE,FALSE,FALSE)
if (dieukien){ # dieukien là một véc-tơ kiểu logic
  print("Xin chào") #R CÓ chạy dòng lệnh này
} # kết thúc câu lệnh if

Bạn đọc có thể sẽ gặp câu lệnh ifelse() trong các đoạn câu lệnh của R. Tuy nhiên đây không phải là cách viết của câu lệnh rẽ nhánh. Hàm ifelse() được sử dụng khi muốn tạo ra một véc-tơ từ hai véc-tơ dựa trên giá trị của một véc-tơ kiểu logic. Cách sử dụng ifelse() được minh họa thông qua ví dụ dưới đây

x<-1:10
ifelse(x%%2==0,"chẵn","lẻ") # tạo ra véc-tơ kiểu chuỗi ký tự
##  [1] "lẻ"   "chẵn" "lẻ"   "chẵn" "lẻ"   "chẵn" "lẻ"   "chẵn" "lẻ"   "chẵn"

Hàm ifelse() ở trên sẽ tạo ra một véc-tơ có độ dài bằng với véc-tơ \(x\) và tương ứng với các vị trí cho kết quả là \(x\) chia hết cho 2 sẽ có giá trị là “chẵn” và tương ứng với các vị trí mà \(x\) không chia hết cho 2 sẽ có gía trị “lẻ”.

Khi sử dụng câu lệnh rẽ nhánh để thực hiện các yêu cầu phức tạp hơn, bạn đọc thường phải sử dụng các câu lệnh ifelse lồng vào nhau để có được kết quả. Bạn đọc có thể quan sát ví dụ sau: để viết một đoạn câu lệnh để trả ra màn hình giá vé vào rạp chiếu phim của một khách hàng dựa trên độ tuổi và việc có thẻ thành viên hay không như bảng ở dưới đây, bạn đọc không thể chỉ dùng một câu lệnh điều kiện duy nhất.

(#tab:unnamed-chunk-85)Ví dụ về câu lệnh điều kiện
Độ tuổi Có phải thành viên Giá vé
Trẻ em (dưới 6 tuổi) Thành viên 70.000 đồng
Người lớn Thành vien 120.000 đồng
Trẻ em (dưới 6 tuổi) Không phải thành viên 100.000 đồng
Người lớn Không phải thành viên 150.000 đồng

Giả sử biến \(Age\) là biến kiểu số cho biết độ tuổi của khách hàng và biến \(Member\) là biến kiểu logic nhận giá trị \(TRUE\) nếu khách hàng là thành viên và \(FALSE\) nếu khách hàng không phải là thành viên. Bạn đọc có thể sử dụng câu lệnh điều kiện để in ra màn hình giá vé của khách hàng đó bằng một trong hai cách như sau

# Cách thứ nhất: sử dụng bốn câu lệnh if
Age<-50; Member<-TRUE # tạo giá trị cho các biến Age, Member
if ((Age < 6) & Member){ # nếu khách hàng dưới 6 tuổi và là thành viên
  print("70.000 đồng")
}
if ((Age < 6) & Member){ # nếu khách hàng trên 6 tuổi và là thành viên
  print("100.000 đồng")
}
if ((Age < 6) & Member){ # nếu khách hàng dưới 6 tuổi và không phải thành viên
  print("120.000 đồng")
}
if ((Age < 6) & Member){ # nếu khách hàng trên 6 tuổi và không phải thành viên
  print("150.000 đồng")
}
# Cách thứ hai: câu lệnh if-else
Age<-50; Member<-TRUE # tạo giá trị cho các biến Age, Member
if (Age<6){
  if(Member){
    print("70.000 đồng")
  } else {
    print("100.000 đồng")
  }
} else {
  if(Member){
    print("120.000 đồng")
  } else {
    print("150.000 đồng")
  }
}

1.4.2 Vòng lặp

Vòng lặp là một cơ chế lập trình với mục đích để R lặp đi lặp lại việc chạy một dòng lệnh hay một đoạn lệnh cụ thể. Có hai kiểu viết lặp đó là vòng lặp for hoạt động theo cách cho một phần tử nhận lần lượt từng giá trị trong một véc-tơ và vòng lặp while hoạt động theo cách lặp lại một đoạn mã cho đến khi một điều kiện cụ thể nhận giá trị là \(FALSE\). Cách thức hoạt động kiểu vòng lặp cũng có thể được áp dụng khi sử dụng nhóm các hàm apply() trong R và sẽ được thảo luận ở một phần riêng của cuốn sách.

1.4.2.1 Vòng lặp for

Những câu lệnh sau dùng để in ra màn hình tất cả các giá trị nằm trong véc-tơ \(qua\) bằng cách sử dụng một vòng lặp for

qua = c("chuối", "táo", "cam", "chanh") # Vec-tơ chứa tên các loại quả
for (ten in qua){ # cho biến ten nhận lần lượt các giá trị trong vec-tơ qua
  print(ten) # in ten ra màn hình
} # kết thúc vòng lặp for
## [1] "chuối"
## [1] "táo"
## [1] "cam"
## [1] "chanh"

Các dòng lệnh bắt đầu từ for đến kết thúc dấu ngoặc \({}\) của vòng lặp có nghĩa là cho một biến \(ten\) nhận lần lượt các giá trị trong véc-tơ \(qua\) từ giá trị ở vị trí thứ nhất đến giá trị ở vị trí cuối cùng. Với mỗi giá trị mà biến \(ten\) nhận được, đoạn lệnh thực hiện nhóm các câu lệnh nằm trong dấu ngoặc \({}\) của vòng lặp for một lần. Trong đoạn lệnh ở trên các câu lệnh được lặp lại là câu lệnh \(print\) với tham số là biến \(ten\).

Bạn đọc hãy thử một ví dụ khó hơn một chút, chẳng hạn như bạn muốn tính tổng các số trong một véc-tơ \(x\) và không sử dụng hàm sum() có sẵn trong R. Bạn có thể thực hiện việc này bẳng một vòng lặp for như sau:

  1. Cho biến tên \(tong\) nhận giá trị bằng 0. \(tong\) sẽ là giá trị của tổng sau khi kết thúc vòng lặp
tong<-0
  1. Cho một biến tên \(gia_tri\) nhận lần lượt các giá trị trong véc-tơ bắt đầu từ vị trí thứ nhất, tại mỗi lần lặp tăng giá trị biến \(tong\) lên đúng bằng giá trị của \(gia_tri\)
for (gia_tri in x){
  tong<-tong + gia_tri 
}
  1. Sau khi vòng lặp \(for\) chạy qua tất cả các giá trị trong véc-tơ cần tính tổng, biến \(tong\) sẽ chứa giá trị của tổng các số trong véc-tơ.
print(tong)

Giả sử \(x\) là véc-tơ \(Airpassengers\) - là một véc-tơ kiểu chuỗi thời gian có sẵn trong R chứa thông tin về số lượng khách hàng đi máy bay hàng tháng, đơn vị là nghìn người, tính từ tháng 1 năm 1949 đến tháng 12 năm 1960. Chúng ta sử dụng vòng lặp for để tính tổng các số trong véc-tơ sau đó so sánh kết quả với hàm sum() có sẵn.

tong<-0 # Tạo biến tên tong nhận giá trị 0
for (gia_tri in AirPassengers){ # cho biến gia_tri nhận lần lượt các giá trị trong Airpassengers
  tong<-tong + gia_tri # tăng tong thêm giá trị bằng gia_tri
} # kết thúc vòng lặp
tong # in tong ra màn hình
## [1] 40363
sum(AirPassengers) # hàm sum() có sẵn cũng cho kết quả tương tự
## [1] 40363

Lời khuyên của chúng tôi là bạn đọc hãy luôn cố gắng viết câu lệnh trong R dưới dạng đối tượng vec-tơ nếu có Sử dụng véc-tơ trong R hiệu quả hơn nhiều cả về thời gian chạy lẫn sự đơn giản của các dòng lệnh. Thật vậy, bạn đọc có thể xem ví dụ dưới đây khi sử dụng vòng lặp for cho những véc-tơ có độ dài lớn và so sánh với tính toán theo vec-tơ. Véc-tơ được sử dụng để kiểm tra tính hiệu quả là véc-tơ có độ dài \(10^9\) (1 tỷ phần tử).

my_vector<-rep(1,10^9)

## Tính tổng véc-tơ có độ dài 10^9 bằng vòng lặp
start<-proc.time()
tong<-0
for (value in my_vector){
  tong<-tong+value
}
proc.time()-start
##    user  system elapsed 
##   17.53    0.00   17.54
## Tính tổng véc-tơ có độ dài 10^9 bằng véc-tơ
start<-proc.time()
tong<-sum(my_vector)
proc.time()-start
##    user  system elapsed 
##    0.81    0.00    0.81

Bạn đọc có thể thấy rằng trên máy tính của chúng tôi, sử dụng vòng lặp for để tính tổng các số trong véc-tơ có độ dài \(10^9\) mất khoảng 25 giây trong khi dùng hàm sum() trực tiếp trên véc-tơ chỉ mất hơn 1 giây.

Trong các ví dụ ở trên, chúng tôi sử dụng trực tiếp giá trị trong véc-tơ để thực hiện vòng lặp. Bạn đọc cũng có thể sử dụng vòng lặp theo chỉ số của véc-tơ và cho kết quả tương tự. Chẳng hạn như đối với véc-tơ \(qua\), bạn đọc có thể cho một chỉ số nhận giá trị lần lượt từ 1 đến độ dài của véc-tơ \(qua\) để lấy từng phần tử của véc-tơ \(qua\):

for (i in 1:length(qua)){ # i sẽ nhận giá trị lần lượt 1,2,3,4
  print(qua[i]) # in ra giá trị thứ i trong véc-tơ qua
} # kết thúc vòng lặp
## [1] "chuối"
## [1] "táo"
## [1] "cam"
## [1] "chanh"

Trong nhiều trường hợp, bạn đọc cần phải sử dụng một vòng lặp \(for\) nằm trong một vòng lặp \(for\) khác để giải quyết được vấn đề của mình. Ví dụ như bạn cần in ra tất cả các cách kết hợp giữa hai cách pha chế là “Nước ép” và “Sinh tố” với bốn loại quả ở trên. Bạn đọc cần sử dụng 2 vòng lặp \(for\) lồng nhau để làm được việc này

pha_che<-c("Nước ép", "Sinh tố") # 2 cách pha chế
for (i in 1:length(pha_che)){ # i sẽ nhận giá trị lần lượt 1,2
  for (j in 1:length(qua)){ # VỚI MỐI i, j sẽ nhận giá trị lần lượt 1,2,3,4
    print(paste(pha_che[i],qua[j],sep=" ")) # in ra màn hình pha chế và quả
  } # kết thúc vòng lặp của j với mỗi i
} # kết thúc vòng lặp của i
## [1] "Nước ép chuối"
## [1] "Nước ép táo"
## [1] "Nước ép cam"
## [1] "Nước ép chanh"
## [1] "Sinh tố chuối"
## [1] "Sinh tố táo"
## [1] "Sinh tố cam"
## [1] "Sinh tố chanh"

Trong ví dụ ở trên, tổng số lần câu lệnh print() được lặp là \(4 \times 2 = 8 (\text{lần})\). Mỗi khi viết vòng lặp for, đặc biệt là khi viết các vòng lặp lồng vào nhau, bạn đọc hãy luôn cân nhắc thời gian R chạy vòng lặp. Một cách để kiểm tra thời gian vòng lặp chạy là thay vì cho chỉ số chạy qua độ dài của cả véc-tơ thì hãy cho vòng lặp thực hiện với một số lượng nhỏ chỉ số ban đầu để ước tính ra tổng thời gian. Nói một cách đơn giản, vòng lặp \(for\) chạy qua 100 giá trị ban đầu của véc-tơ sẽ mất thời gian bằng khoản \(\cfrac{1}{100}\) thời gian để chạy vòng lặp qua 10.000 giá trị của toàn bộ véc-tơ. Thời gian để thực hiện các vòng lặp for lồng nhau sẽ tăng lên theo cấp số nhân.

1.4.2.2 Vòng lặp while

Vòng lặp for được gọi là vòng lặp xác định vì nếu không có thêm các câu lệnh đặc biệt, người viết câu lệnh sẽ biết trước được số lần vòng lặp thực hiện. Một cách khác để thực hiện vòng lặp là sử dụng vòng lặp while. Đây là kiểu vòng lặp không xác định, nghĩa là trong nhiều trường hợp người viết câu lệnh sẽ không biết trước được sẽ vòng lặp sẽ được thực hiện bao nhiêu lần. Trước khi nói kỹ hơn về khái niệm không xác định, bạn đọc hãy làm quen với cấu trúc của vòng lặp while trước. Cách viết một vòng lặp while như sau

while (y){ # y là một biến kiểu logic
  "Đoạn câu lệnh"
}

Nguyên tắc hoạt động của vòng lặp while là thực hiện “Đoạn câu lệnh” nằm giữa dấu \({}\) nếu giá trị của \(y\)\(TRUE\) và bỏ qua vòng lặp nếu giá trị của \(y\)\(FALSE\). Nếu \(y\) nhận giá trị là \(TRUE\) và trong “Đoạn câu lệnh” không có các dòng lệnh tác động làm thay đổi giá trị của \(y\) thì \(y\) sẽ luôn luôn nhận giá trị là \(TRUE\) và khi đó vòng lặp sẽ lặp vô hạn.

Vòng lặp while dưới đây sẽ in ra tên các phần tử của véc-tơ \(qua\) bằng cách sử dụng một chỉ số tăng dần và chỉ thoát ra khỏi vòng lặp nếu chỉ số đó vượt qua độ dài của véc-tơ:

qua = c("chuối", "táo", "cam", "chanh") # Vec-tơ chứa tên các loại quả
i<-1
while (i <= length(qua)){ # TRUE cho đến khi i = 5
  print(qua[i]) # in ra màn hình phần tử thứ i
  i<-i+1 # tăng i lên dần để thoát ra khỏi vòng lặp
} # kết thúc vòng lặp while
## [1] "chuối"
## [1] "táo"
## [1] "cam"
## [1] "chanh"
print(i) # kiểm tra giá trị của i khi thoát ra khỏi vòng lặp
## [1] 5

Trong ví dụ ở trên chúng ta đã biết chính xác khi nào chúng ta sẽ dừng lại vòng lặp nên việc sử dụng vòng lặp while sẽ phức tạp hơn vòng lặp for. Vòng lặp while sẽ phát huy hiệu quả khi bạn đọc không biết chính xác khi nào chúng ta nên dừng việc thực hiện lặp các câu lệnh.

Hãy lấy ví dụ khi bạn đọc muốn kiểm tra xem một số tự nhiên \(n\) bất kỳ có phải là số nguyên tố hay không. Xin được nhắc lại rằng số nguyên tố là các số tự nhiên chỉ có hai ước số là số 1 và chính nó. Để kiểm tra xem số \(n\) có phải là số nguyên tố hay không, bạn đọc cần kiểm tra xem \(n\) có chia hết cho số nguyên dương nào từ 2 đến số tự nhiên là phần nguyên của \(\sqrt{n}\) hay không. Số phần nguyên của \(\sqrt{n}\) ký hiệu là \([\sqrt{n}]\). Nếu \(n\) chia hết cho một số bất kỳ từ 2 đến \([\sqrt{n}]\), \(n\) không phải là số nguyên tố. Theo nguyên tắc này bạn đọc có thể viết một vòng lặp for chạy từ \(2\) đến \([\sqrt{n}]\) và kiểm tra xem \(n\) có chia hết cho số nào trong dãy này không. Tuy nhiên vòng lặp for như vậy sẽ luôn luôn phải lặp lại \([\sqrt{n}] - 1\) lần. Viết vòng lặp while trong trường hợp này sẽ hiệu quả hơn rất nhiều bởi chỉ cần \(n\) chia hết cho 1 số nào đó chúng ta có thể kết thúc ngay vòng lặp và kết luận \(n\) không phải là số nguyên tố.

n<-123454321 # số nguyên dương bất kỳ
ket_qua<-TRUE # kết quả sẽ thay đổi nếu n chia hết cho 1 số nào đó
uoc_so<-2
while( ket_qua & (uoc_so < n^0.5) ){ # tiếp tục lặp nếu ket_qua = TRUE VÀ ước số < n^0.5
  if(n %% uoc_so == 0){
    ket_qua<-FALSE # thay đổi giá trị của ket_qua nếu n chia hết cho uoc_so
  }
  uoc_so<-uoc_so + 1 # tăng ước số thêm 1
}
ket_qua # TRUE nến n nguyên tố
## [1] FALSE

Hãy thử áp dụng vòng lặp while trên một ví dụ khác liên quan đến dữ liệu \(trump\_tweet\). Chẳng hạn như bạn đọc muốn tìm ra thời điểm đầu tiên mà một câu tweet được like nhiều hơn 10.000 lần. Câu hỏi này khá dễ nếu chúng ta tư duy theo tương tác véc-tơ. Tuy nhiên chúng tôi muốn bạn đọc suy nghĩ theo hướng sử dụng vòng lặp. Chúng ta sẽ sử dụng một chỉ số tăng dần từ 1 và kiểm tra xem câu tweet đó có nhiều hơn 10.000 like hay không và chỉ dừng lại việc kiểm tra nếu gặp câu tweet nhiều hơn 10.000 like. Chúng ta không biết chính xác khi nào sẽ dừng lại, do đó sử dụng vòng lặp while sẽ hợp lý trong trường hợp này

kiem_tra<-TRUE
i<-0
while(kiem_tra){ # chắc chắn có câu nhiều hơn 10.000 like nên không cần hạn chế i
  i<-i+1 # tăng chỉ số i
  kiem_tra<-trump_tweets$favorite_count[i] <= 10^4 # tiếp tục lặp nếu số like <=10^4
}
trump_tweets$favorite_count[i] # chỉ số i là chỉ số nhỏ nhất mà số like nhiều hơn 10.000
## [1] 15457
trump_tweets$created_at[i] # thời điểm viết câu tweet đó
## [1] "2011-12-21 15:36:36 EST"

Mặc dù phần này của cuốn sách đang viết về vòng lặp nhưng chúng tôi muốn nhắc lại rằng bạn đọc hãy cố gắng sử dụng véc-tơ để tìm lời giải thay vì sử dụng vòng lặp khi có thể. Cùng câu hỏi như trên, chúng ta có thể cho lời giải đơn giản hơn bằng cách sử dụng hàm match().

vitri<-match(TRUE,trump_tweets$favorite_count>10^4) # vitri là chỉ số nhỏ nhất mà số like nhiều hơn 10.000
trump_tweets$created_at[vitri] # thời điểm viết câu tweet đó
## [1] "2011-12-21 15:36:36 EST"

Khi làm việc với vòng lặp while những người mới làm quen với lập trình rất dễ rơi vào trạng thái vòng lặp vô hạn. Dưới đây là một ví dụ về một vòng lặp như vậy. Biến \(kiem\_tra\) nhận giá trị ban đầu là \(TRUE\) và trong các câu lệnh nằm trong vòng lặp không có câu lệnh nào tác động đến giá trị của biến đó. Bạn đọc sẽ thấy giá trị \(i\) được in ra tăng dần và không bao giờ dừng lại. Bạn đọc chỉ có thể dừng chương trình chạy bằng cách nhấn vào biểu tưởng “STOP” phía trên bên phải cửa sổ R console.

# HÃY CẨN THẬN VÌ ĐÂY LÀ VÒNG LẶP VÔ HẠN
kiem_tra<-TRUE
while (kiem_tra){ # kiem_tra luôn luôn nhận giá trị TRUE
  print(paste0("Giá trị của i hiện tại: ", i)) # in ra màn hình phần tử thứ i
}

Kinh nghiệm của chúng tôi khi sử dụng vòng lặp không xác định là luôn luôn sử dụng một biến, tạm gọi là \(i\), không liên quan đến chương trình chạy và được gán cho giá trị tăng dần trong vòng lặp. Trong biển thức điều kiện luôn luôn kèm thêm một điều kiện là \(i\) nhỏ hơn số lần lặp tối đa mà người lập trình quy định. Bạn đọc có thể quan sát đoạn lệnh sau:

loop_max<-10^4
i<-1 # i ban đầu là 1
kiem_tra<-TRUE
while (kiem_tra & (i<= loop_max)){
  i<-i+1 # luôn luôn tăng i
  print(paste0("Giá trị của i hiện tại: ", i)) # in ra màn hình phần tử thứ i
}

Các đoạn câu lệnh kiểu trên sẽ lặp tối đa là 10.000 lần do chúng ta sử dụng thêm điều kiện (i<= loop_max)

1.4.2.3 Điều khiển vòng lặp

Khi bạn đọc viết các vòng lặp for hoặc while, R cung cấp các từ khóa để bạn đọc có thể điều khiển vòng lặp. Các từ khóa đó bao gồm breaknext. Ý nghĩa của các từ khóa này như sau

(#tab:unnamed-chunk-101)Các từ khóa điều khiển vòng lặp
Từ khóa Ý nghĩa
next Chuyển tới bước lặp tiếp theo, bỏ qua các câu lệnh còn lại trong vòng lặp hiện tại
break Dừng vòng lặp ngay lập tức

Bạn đọc quan sát giá trị trả ra màn hình của đoạn câu lệnh sau đề hiểu cách sử dụng next trong vòng lặp

qua = c("chuối", "táo", "cam", "chanh") # Vec-tơ chứa tên các loại quả
for (ten in qua){ # cho biến ten nhận lần lượt các giá trị trong vec-tơ qua
  if (ten == "cam"){
    next # nếu ten là "cam" thì chuyển qua vòng lặp tiếp theo
  }
  print(ten) # in ten ra màn hình
} # kết thúc vòng lặp for
## [1] "chuối"
## [1] "táo"
## [1] "chanh"

Có thể thấy rằng trong các loại quả được in ra màn hình không có giá trị \(cam\) bởi vì khi biến \(ten\) bằng giá trị này từ khóa next đã kết thúc vòng lặp hiện tại, bỏ qua dòng lệnh print() và đi đến vòng lặp tiếp theo. Vẫn các câu lệnh như trên nhưng thay next bằng break, chúng ta có thể quan sát R trả ra kết quả như sau

qua = c("chuối", "táo", "cam", "chanh") # Vec-tơ chứa tên các loại quả
for (ten in qua){ # cho biến ten nhận lần lượt các giá trị trong vec-tơ qua
  if (ten == "cam"){
    break # nếu ten là "cam" thì kết thúc vòng lặp ngay lập tức
  }
  print(ten) # in ten ra màn hình
} # kết thúc vòng lặp for
## [1] "chuối"
## [1] "táo"

R chỉ trả ra tên hai loại quả là \(chuối\)\(táo\) bởi vì khi gặp giá trị \(cam\) từ khóa break đã kết thúc vòng lặp for.

Trong R còn có một kiểu viết vòng lặp không xác định khác với vòng lặp while đó là viết vòng lặp sử dụng câu lệnh repeat. Khi sử dụng vòng lặp repeat bạn đọc luôn luôn phải sử dụng từ khóa break để kết thúc vòng lặp và tránh bị lặp vô hạn. Cách sử dụng repeat trong R như sau

qua = c("chuối", "táo", "cam", "chanh") # Vec-tơ chứa tên các loại quả
i<-0
repeat{
  i<-i+1 # luôn luôn tăng i
  print(qua[i]) # in tên ra màn hình
  if (i== length(qua)){break}
} # kết thúc vòng lặp repeat
## [1] "chuối"
## [1] "táo"
## [1] "cam"
## [1] "chanh"

Trong vòng lặp repeat ở trên, chúng tôi sử dụng điều kiện là \(i\) bằng độ dài của véc-tơ để kết thúc vòng lặp. Những bạn đọc mới làm quen với lập trình sẽ dễ bị nhầm lẫn về cách kết thúc vòng lặp của whilerepeat. Cách hoạt động của hai vòng lặp này là tương đương nhau nên chúng tôi cho rằng những bạn đọc chưa quen với lập trình nên chỉ chọn một trong hai cách viết trong quá trình viết câu lệnh.

1.4.3 Viết hàm số

Hàm số có vai trò quan trọng trong R trong tất cả các ngôn ngữ lập trình nào khác. Hàm số đảm bảo sự chính xác và tiện lợi khi lập trình trình và hàm số là phương pháp chuyển giao kiến thức và kinh nghiệm hiệu quả từ người dùng này đến người dùng khác.

  1. Hàm số đặc biệt có ý nghĩa khi bạn phải thực hiện một đoạn câu lệnh một cách lặp đi lặp lại và do sự thay đổi của một số yếu tố đầu vào. Thay vì phải làm đi làm lại công việc đó một cách thủ công, bạn hãy viết quy trình đó thành một hàm số.

  2. Khi chúng ta muốn chuyển giao kinh nghiệm, kiến thức của mình cho một người khác, hãy viết chương trình của bạn dưới dạng hàm số và chuyển giao. Người dùng có thể không hiểu được ý nghĩa của chương trình của bạn ngay thì ít nhất cũng có thể sử dụng được kiến thức của bạn. Nếu bạn đọc để ý các thư viện cài đặt thêm trên R đều là tập hợp của các hàm số.

Hàm số trên R ngoài các hàm sẵn có ngay khi bạn cài R, các hàm số nằm trong các thư viện mà bạn đọc cài đặt bổ sung, và các hàm số mà bạn đọc tự định nghĩa.

1.4.3.1 Hàm số do người dùng tự định nghĩa.

Từ khóa để khai báo một hàm số là function(). Để tự tạo một hàm số tên là \(f\) nhận giá trị là \(x^2\) thì bạn đọc sử dụng đoạn câu lệnh như sau:

f<-function(x){ # là một hàm số của biến x
  return(x^2) # trả lại giá trị của hàm số là x^2
}

Thay vì sử dụng từ khóa return, bạn đọc cũng có thể sử dụng tên hàm số đển gán cho giá trị trả lại:

f<-function(x){ # là một hàm số của biến x
  f<-x^2 # trả lại giá trị của hàm số là x^2
}

Đôi khi bạn đọc sẽ gặp các đoạn lệnh khai báo hàm số không có từ khóa return và cũng không có phần gán giá trị cho hàm số. Khi đó R sẽ luôn luôn lấy giá trị được trả ra cuối cùng để gán giá trị cho hàm số đó.

f<-function(x) x^2

Cách viết này chỉ phù hợp cho các hàm số ngắn gọn và chúng tôi khuyên bạn đọc hãy luôn sử dụng từ khóa return khi trả lại giá trị cho hàm số.

Sau khi đã chạy các đoạn lệnh khai báo hàm số \(f(x) = x^2\), R sẽ lưu đối tượng có tên \(f\) là kiểu hàm số lên môi trường làm việc chung. Để gọi hàm số và thực hiện tính toán, bạn đọc cần viết đúng tên hàm và cho tham số \(x\) giá trị phù hợp.

class(f) # kiểu của đối tượng f là function
## [1] "function"
f(10) # cho tham số x giá trị bằng 10
## [1] 100

Từ khóa return() được sử dụng để trả lại giá trị cho hàm số \(f\) và R sẽ gán giá trị cho hàm số \(f\) ngay lập tức khi gặp hàm câu lệnh \(return()\). Nếu trong đoạn câu lệnh của hàm số \(f\) có nhiều từ khóa return, giá trị của \(f\) sẽ được gán bằng từ khóa return đầu tiên. Hãy quan sát ví dụ sau:

f<-function(x){ # là một hàm số của biến x
  return(x^2) # trả lại giá trị của hàm số là x^2 khi gặp return
  return(x^3) # R sẽ không chạy câu lệnh này
}
f(10) # trả lại gái trị là 100

Cách đặt tên hàm số cũng giống như đặt tên biến trong R, bạn đọc cần lựa chọn tên hợp lệ và tránh các từ khóa. Biến \(x\) trong phần khai báo hàm số ở trên được gọi là biến, tham số, hoặc tùy biến. Hàm số trong R có thể không có tham số nào hoặc có thể có rất nhiều tham số, mỗi tham số là một kiểu đối tượng khác nhau, việc này hoàn toàn tùy thuộc vào người lập trình. Bên trong dấu \(\{\}\) của từ khóa function() được gọi là môi trường cục bộ, R sẽ luôn ưu tiên biến nằm trong môi trường này trước tất cả các môi trường khác. Vấn đề sẽ được thảo luận ở phần tiếp theo. Một điểu cần lưu ý là khi viết hàm số hãy luôn luôn có tài liệu đi kèm rõ ràng để người sử dụng khác, hoặc chính mình khi sử dụng có thể hiểu hay nhớ được hàm số được sử dụng như thế nào và với mục đích gì.

Tham số hay biến số là phần thiết yếu của các hàm trong R. Trong phần tiếp theo, chúng ta sẽ xem xét cách tham số trong hàm số hoạt động như thế nào, chẳng hạn như cách tạo giá trị mặc định cho tham số, cách xử lý các giá trị tham số bị thiếu, cách bổ sung vào tham số bằng cách sử dụng dấu ba chấm \(...\).

Để tạo giá trị mặc định cho tham số bạn đọc cần tạo giá trị phù hợp khi khai báo hàm số. Tạo giá trị mặc định cho tham số là quan trọng khi bạn đọc viết các hàm số có nhiều tham số bởi vì khi bạn gọi hàm số và quên tạo giá trị cho một vài tham số nào đó, R sẽ sử dụng giá trị mặc định để tính toán. Hãy xem xét ví dụ sau: bạn muốn viết một hàm số để tính giá trị hiện tại (present value) của một dòng tiền được quan sát theo năm và được lưu trong một véc-tơ tên là \(CF\). Lãi suất tính theo kiểu lãi gộp là \(i\). Chúng ta sẽ sử dụng giá trị mặc định là \(5\%\) để gán cho \(i\)

PV<-function(i = 0.05, CF){  # Hàm số tính giá trị hiện tại của dòng tiền CF
  n<-length(CF)
  discount_factor<-(1+i)^(-(1:n))
  return (sum(discount_factor * CF))
}

Giả sử dòng tiền có giá trị là $1.000 tại thời điểm 1 và tăng dần $1000 mỗi năm và lên đến $10.000 tại năm thứ 10. Mức lãi suất gộp \(i = 10\%/năm\). Giá trị hiện tại của dòng tiền được tính bằng hàm \(PV\) như sau

MyCF<-seq(1000,10000,length=10)
PV(i = 0.1, MyCF) # Giá trị hiện tại của dòng tiền MyCF tại lãi suất 10%/năm
## [1] 29035.91

Khi chúng ta quên không gán giá trị cho tham số \(i\) khi gọi hàm \(PV\), R sẽ cho \(i\) nhận giá trị mặc định là \(5\%\)

PV(CF = MyCF) # Giá trị hiện tại của dòng tiền MyCF tại lãi suất 5%/năm
## [1] 39373.78

Sử dụng dấu ba chấm \(...\) khi khai báo tham số của một hàm số là phương pháp để người lập trình sử dụng tham số có sẵn của một hàm số khác. Nguyên tắc hoạt động của cách khai báo tham số này thể hiện qua ví dụ sau: hàm \(PV\) được xây dựng ở trên chỉ tính được dòng tiền tại các thời điểm cuối các năm. Bạn đọc muốn hàm \(PV\) có thể tính được giá trị hiện tại của dòng tiền trong cả hai trường hợp: dòng tiền bắt đầu từ thời điểm đầu năm hoặc dòng tiền bắt đầu từ cuối năm; bằng cách thêm vào một tham số \(bat\_dau\); khi \(bat\_dau = 0\) thì thời điểm bắt đầu là đầu năm thứ nhất và khi \(bat\_dau = 1\) thì thời điểm bắt đầu là cuối năm thứ nhất. Thay vì sửa lại làm \(PV\) chúng ta có thể viết một hàm mới, tạm gọi là \(PV1\) và sử dụng tham số của hàm \(PV\)

PV1<-function(bat_dau,i,...){ # chúng ta chỉ sử dụng tham số i của PV, các tham số khác khai báo bằng ...
  if (bat_dau==1) {
    return (PV(i,...)) # PV1 sử dụng các tham số còn lại của PV
  } else {
    return ( (1+i)*PV(i,...) ) # PV1 sử dụng các tham số còn lại của PV
  }
}

Khi gọi hàm \(PV1\) chúng ta cần gọi đầy đủ tham số:

PV1(bat_dau = 0,i = 0.1, CF = MyCF) # dòng tiền bắt đầu từ đầu năm thứ 1
## [1] 31939.5
PV1(bat_dau = 1,i = 0.1, CF = MyCF) # dòng tiền bắt đầu từ cuối năm thứ 1
## [1] 29035.91

Bạn đọc cũng có thể sử dụng cách mượn tham số này để sử dụng các hàm số có sẵn trong R. Trong ví dụ dưới đây, chúng tôi tự xây dựng một hàm có tên là myplot() để vẽ đồ thị rải điểm của một véc-tơ kiểu số \(x\) theo chỉ số của véc-tơ đó đồng thời và mượn các tham số \(main\), \(xlab\), \(ylab\) của hàm plot() có sẵn:

myplot<-function(x,...){ # hàm myplot vẽ đồ thị rải điểm
  n<-length(x) # độ dài của véc-tơ x
  plot(1:n,x,...)
}

Chúng ta sẽ sử dụng hàm myplot() để vẽ đồ thị rải điểm của véc-tơ \(x\) nhận giá trị bằng véc-tơ kiểu chuỗi thời gian \(AirPassengers\).

myplot(AirPassengers,main="Số lượng hành khách các tháng", 
       ylab = "Số lượng hành khách", # tùy biến ylab của hàm plot()
       xlab = "", # tùy biến xlab của hàm plot()
       type = "l", color = "red") # tùy biến type và color của hàm plot

Bạn đọc có thể tham khảo cách sử dụng hàm plot() trong phần đồ thị cơ bản trong cuốn sách này.

1.4.3.2 Hàm số được xây dựng sẵn

Hàm số được xây dựng sẵn là các hàm số được phát triển sẵn trong R và các hàm số trong các thư viện mà bạn đọc cài đặt thêm cho R. Để biết R hiện đang có các thư viện nào đang sẵn sàng để sử dụng, bạn đọc chỉ cần sử dụng câu lệnh

search() # liệt kê danh sách các đối tượng, thư viện đang sẵn có theo thứ tự ưu tiên
##  [1] ".GlobalEnv"         "package:dslabs"     "package:ggrepel"   
##  [4] "package:pryr"       "package:gridExtra"  "package:grid"      
##  [7] "package:forcats"    "package:ggplot2"    "package:kableExtra"
## [10] "package:knitr"      "package:dplyr"      "package:readxl"    
## [13] "package:stats"      "package:graphics"   "package:grDevices" 
## [16] "package:utils"      "package:datasets"   "package:methods"   
## [19] "Autoloads"          "package:base"

Đa số các phiên bản R đều có sẵn các thư viện như \(stats\), \(graphics\), \(utils\),… Để biết cụ thể hơn trong một thư viện có những đối tượng (hàm số, nhóm các hàm số) nào khác, chúng ta sử dụng câu lệnh

library(help = "stats") # liệt kê các đối tượng trong thư viện stats

Bạn đọc sẽ thấy cửa sổ Script liệt kê ra danh sách các hàm số hoặc tên các đối tượng lưu chứa nhóm các hàm số đã được phát triển sẵn trong thư viện \(stats\). Một vài đối tượng được liệt kê ra trong danh sách là các hàm số: hàm AIC(), hàm ARMAacf,… Một số đối tượng là nhóm các hàm số, chẳng hạn như Beta hay Binomal. Khi bạn đọc thử gọi \(Beta\) trên cửa sổ Console sẽ gặp lỗi vì đó không phải là tên chính xác của hàm số. Thay vì thế hãy chạy câu lệnh \(? Beta\) để thấy rằng trong đối tượng \(Beta\) của thư viện \(stats\) có một nhóm các hàm số liên quan đến phân phối xác suất \(Beta\): hàm dbeta(), hàm pbeta(), hàm qbeta(), và hàm rbeta().

Chúng tôi không bàn đến việc làm thế nào để biết sử dụng hàm số nào trong một trường hợp cụ thể bởi vì đương nhiên không có câu trả lời chung cho câu hỏi này. Việc này tùy thuộc vào chuyên môn, hiểu biết, khả năng tìm kiếm của bạn đọc. Chúng tôi muốn tập trung vào việc đảm bảo bạn đọc gọi đúng hàm số mà bạn mong muốn. Sẽ không có vấn đề lớn nếu tên hàm số bạn cần gọi là duy nhất trên cửa số R bạn đang làm việc. Tuy nhiên, khi có một vài đối tượng khác có tên giống như tên hàm số bạn đang sử dụng, bạn sẽ gặp vấn đề.

Để làm được việc này bạn đọc nên hiểu một chút về môi trường làm việc và thứ tự ưu tiên khi gọi tên một đối tượng trong R. Khi bạn làm việc trên R, có ba môi trường mà R sử dụng để lưu trữ các đối tượng. Môi trường thứ nhất tạm gọi là môi trường chung (thuật ngữ công nghệ thông tin gọi là toàn cục), thứ hai là môi trường các thư viện, và cuối cùng là môi trường trong một hàm số cụ thể (thuật ngữ CNTT gọi là cục bộ). Khi bạn gọi tên một đối hay một hàm số, R sẽ luôn luôn ưu tiên theo thứ tự là: môi trường cục bộ \(\rightarrow\) môi trường chung (toàn cục) \(\rightarrow\) môi trường các thư viện. Do có nhiều thư viện cùng mở trên R nên để biết thứ tự ưu tiên của các thư viện bạn đọc sử dụng hàm search(). Các thư viện được ưu tiên hơn sẽ có chỉ số nhỏ hơn (xuất hiện) trước khi sử dụng search().

Nhìn chung các thư viện cài đặt thêm sẽ thường được ưu tiên hơn các thư viện có sẵn. Nếu một hàm trong thư viện cài đặt thêm trùng tên với một hàm trong thư viện có sẵn, R ưu tiên thư viện cài đặt thêm. Thật vậy, hàm số tên filter() là một hàm được xây dựng sẵn trong thư viện \(stats\). Tuy nhiên trong thư viện \(dplyr\) cũng có một hàm tên là filter(). Trước khi gọi thư viện \(dplyr\), mỗi khi bạn đọc gọi hàm filter(), R sẽ luôn hiểu đây là hàm filter() của thư viện \(stats\).

? filter # nếu chưa gọi thư viện dplyr, filter là hàm của thư viện stats

Sau khi chúng ta gọi thư viện \(dplyr\), chúng ta sẽ thấy thư viện \(dplyr\) xuất hiện trước thư viện \(stats\) theo thứ tự ưu tiên.

library(dplyr) # gọi thư viện dplyr 
search() # sau khi gọi thư viện dplyr, thư viện này được ưu tiên trước stats

Trong thư viện \(dplyr\) cũng có một hàm tên là filter(). Theo thứ tự ưu tiên nếu bạn đọc gọi hàm filter() thì R sẽ hiểu đây là hàm filter của thư viện \(dplyr\). Lúc này muốn sử dụng hàm filter() của thư viện \(stats\) bạn đọc cần phải sử dụng tên thư viện viết trước hàm này stats::filter().

Như đã nói ở phần trước, môi trường chung cũng là môi trường được ưu tiên trước môi trường các thư viện. Bạn đọc có thể thấy từ kết quả hàm search(), môi trường chung, ký hiệu \(.GlobalEnv\), luôn xuất hiện trước tiên. Môi trường chung chính là nơi lưu trữ tất cả các hàm số hay đối tượng mà bạn đọc tự định nghĩa. Môi trường chung luôn được ưu tiên trước môi trường thư viện. Điều này có nghĩa là nếu bạn đọc tự định nghĩa một biến, một véc-tơ, hay hàm số có tên là \(filter\), R sẽ ưu tiên tên \(filter\) cho đối tượng mà bạn đọc tự định nghĩa. Như vậy, nếu bạn đọc sử dụng tên \(filter\) cho một hàm bạn tự định nghĩa, bạn sẽ cần phải sử dụng thêm tên thư viện để gọi hàm filter() từ các thư viện \(dplyr\) hoặc \(stats\).

Còn một môi trường khác, tạm gọi là môi trường cục bộ, sẽ được ưu tiên hơn môi trường chung. Môi trường cục bộ mô tả môi trường bên trong một hàm số mà bạn đọc tự định nghĩa. Giả sử sau khi bạn đọc tự định nghĩa một hàm filter() trên môi trường chung và sau đó tự định nghĩa một hàm số \(f\) có sử dụng một tham số (có thể là biến hoặc hàm số) có tên là \(filter\) thì mỗi khi bạn đọc gọi hàm số \(f\), đối tượng tên \(filter\) sẽ luôn được hiểu là tham số của hàm số \(f\). Môi trường bên trong hàm \(f\) được gọi là môi trường cục bộ. Bạn đọc hãy quan sát ví dụ dưới đây để hiểu hơn về môi trường chung và môi trường cục bộ

filter<-function(){return(pi)} #Tự định nghĩa hàm filter trong môi trường chung
filter() # hàm filter trong .GlobalEnv luôn bằng pi 
## [1] 3.141593

Trước hết chúng ta định nghĩa một hàm tên là \(filter()\) trong môi trường chung luôn nhận giá trị bằng hằng số \(\pi\). Lúc này khi chúng ta gọi filter(), R sẽ hiểu rằng đây là hàm filter() do chúng ta tự định nghĩa. Sau đó chúng ta định nghĩa một hàm số tên là \(f\) đồng thời bên trong hàm \(f\) chúng ta định nghĩa một hàm filter() khác nhận giá trị là 10. Hàm filter() bên trong hàm f được gọi là hàm số trong môi trường cục bộ.

f<-function(){
  filter<-function(){return(10)}  # bên trong hàm f, định nghĩa lại hàm filter bằng 10
  return(filter())
}

Khi chúng ta gọi hàm số f, hàm số này lại gọi một hàm số tên là filter được định nghĩa bên trong hàm số nó. Bởi vì R ưu tiên môi trường cục bộ trước môi trường hàm \(filter\) bên trong f có giá trị bằng 10. Bên ngoài hàm số f, chúng ta lại gọi filter() thì giá trị trả lại là \(\pi\) vì đây là môi trường chung.

f() # trả lại giá trị là 10 vì hàm filter bên trong f nhận giá trị 10
## [1] 10
filter() # trả lại giá trị pi
## [1] 3.141593

Tất cả các hàm số mà bạn đọc thường xuyên sử dụng hãy lưu trong các file và mỗi khi cần sử dụng bạn đọc chỉ cần gọi tên file đó thay vì copy toàn bộ các câu lệnh của các hàm số vào cửa sổ Script. Hàm số để gọi một file lên cửa sổ R bạn đang sử dụng là hàm source(). Chẳng hạn như tất cả các hàm số bạn đọc tự định nghía được lưu ở một file có tên là “myfunctions.R”, bạn chỉ cần sử dụng câu lệnh sau để gọi tất cả các hàm số lên cửa sổ đang làm việc:

source("Đường dẫn đến file/myfunction.R")

1.5 Đồ thị cơ bản

1.6 Phụ lục

2 Ma trận, mảng nhiều chiều và \(list\)

Trong cuốn sách này chúng tôi cố gắng tránh nhắc đến các khái niệm toán học phức tạp bởi đối tượng chúng tôi hướng đến là những người làm việc với dữ liệu nhưng không có một nền tảng chuyên sâu về toán học. Tuy nhiên để làm việc được với dữ liệu thì các kiến thức về ma trận nói riêng và kiến thức về đại số tuyến tính nói chung là bắt buộc phải nắm vững. Điều đáng tiếc là tại thời điểm chúng tôi viết cuốn sách này, đa số các chương trình đào tạo dành cho sinh viên các ngành kinh tế đang cắt giảm dần kiến thức về toán học và đặc biệt là kiến thức đại số tuyến tính.

2.1 Ma trận

Ma trận có ý nghĩa đặc biệt quan trọng trong phân tích dữ liệu bởi đa số các dữ liệu đều được chuyển thành kiểu ma trận để dễ dàng phân tích và tính toán. Cũng giống như véc-tơ, ma trận là một đối tượng dùng để lưu các biến có cùng kiểu. Khác với véc-tơ, ma trận lưu phần tử theo hàng và cột, nghĩa là trong không gian hai chiều trong khi véc-tơ lưu phần tử trong không gian một chiều. Bạn đọc cũng có thể hiểu véc-tơ là một cột trong khi ma trận là tập hợp của các cột có cùng độ dài. Kích thước của một véc-tơ là chiều dài của véc-tơ đó trong khi kích thước của một ma trận là số hàng và số cột của ma trận đó.

2.1.1 Khởi tạo ma trận

Hàm số dùng để tạo ra ma trận trong R là hàm matrix(). Khi tạo ma trận, bạn đọc sẽ luôn luôn phải khởi tạo giá trị cho ma trận đó. Đoạn lệnh sau sẽ khởi tạo một ma trận có tên là \(M\), có 3 hàng, 4 cột, và giá trị trong ma trận là các số tự nhiên từ 1 đến 12 được sắp xếp theo thứ tự

M<-matrix(1:12, nrow = 3, ncol = 4) # nrow: số hàng, ncol: số cột
M # in M ra của sổ console
##      [,1] [,2] [,3] [,4]
## [1,]    1    4    7   10
## [2,]    2    5    8   11
## [3,]    3    6    9   12

Các giá trị dùng để khởi tạo cho ma trận là các số từ 1 đến 12 và được điền vào ma trận \(M\) theo nguyên tắc từ trên xuống dưới rồi từ trái sang phải, nghĩa là cột thứ nhất sẽ được ưu tiên cho giá trị trước; phần tử hàng thứ nhất của cột thứ nhất sẽ được điền giá trị trước, sau đó đến phần tử ở hàng thứ hai của cột thứ nhất, …; sau khi hết cột thứ nhất R sẽ tiếp tục điền vào giá trị ở hàng thứ nhất của cột thứ hai,…, và cứ tiếp tục như thế sau khi tất cả các phần tử trong ma trận đều có giá trị. Véc-tơ dùng đề khởi tạo giá trị cho ma trận có độ dài 12 vừa đúng với số phần tử trong ma trận nên câu lệnh tạo ma trận \(M\) ở trên hoạt động bình thường. Trong trường hợp bạn đọc sử dụng véc-tơ có độ dài khác 12 để khởi tạo giá trị cho ma trận, câu lệnh vẫn sẽ chạy nhưng có kèm theo cảnh báo:

M<-matrix(1:13, nrow = 3, ncol = 4) # Code chạy kèm theo cảnh báo; 
M<-matrix(1:5, nrow = 3, ncol = 4) # Code chạy kèm theo cảnh báo; 

Bạn đọc có thể thấy rằng:

  • Nếu véc-tơ dùng để khởi tạo giá trị cho ma trận \(M\) có độ dài lớn hơn 12, R sẽ dùng 12 giá trị đầu tiên để khởi tạo giá trị cho ma trận.

  • Nếu véc-tơ dùng để khởi tạo giá trị cho ma trận \(M\) có độ dài nhỏ hơn 12, R sẽ lặp lại véc-tơ đó cho đến khi có độ dài lớn hơn hoặc bằng 12 rồi sau đó dùng 12 giá trị đầu tiên để khởi tạo giá trị cho ma trận.

Khi khởi tạo ma trận, bạn đọc có thể yêu cầu giá trị được khởi tạo theo hàng thay vì theo cột bằng tùy biến \(byrow = TRUE\) trong hàm matrix().

M<-matrix(1:12, nrow = 3, ncol = 4, byrow = TRUE) # nrow: số hàng, ncol: số cột
M # in M ra của sổ console
##      [,1] [,2] [,3] [,4]
## [1,]    1    2    3    4
## [2,]    5    6    7    8
## [3,]    9   10   11   12

Để biết kích cỡ của ma trận, chúng ta sử dụng hàm dim(). Hàm dim() trên ma trận sẽ trả lại giá trị là một véc-tơ kiểu số có độ dài là hai, phần tử thứ nhất là số hàng, phần tử thứ hai là số cột của ma trận

dim(M) # ma trận 3 hàng 4 cột
## [1] 3 4

Ma trận cũng có thể được khởi tạo bằng cách ghép các véc-tơ hoặc các ma trận khác theo hàng hay theo cột bằng các hàm cbind() hoặc rbind():

  • Hàm cbind() nối các ma trận có cùng số hàng hoặc ma trận với véc-tơ có độ dài bằng số hàng của ma trận.

  • Tương tự, rbind() nối các ma trận có cùng số cột hoặc ma trận với véc-tơ có độ dài bằng số cột của ma trận.

cbind(M,rep(1,3)) # ghép THEO CỘT, ma trận M (3 hàng, 4 cột) với véc-tơ độ dài 3
##      [,1] [,2] [,3] [,4] [,5]
## [1,]    1    2    3    4    1
## [2,]    5    6    7    8    1
## [3,]    9   10   11   12    1
rbind(M,rep(1,4)) # ghép THEO HÀNG, ma trận M (3 hàng, 4 cột) với véc-tơ độ dài 4
##      [,1] [,2] [,3] [,4]
## [1,]    1    2    3    4
## [2,]    5    6    7    8
## [3,]    9   10   11   12
## [4,]    1    1    1    1

Các phép tính toán thông thường trên ma trận cũng có nguyên tắc giống như đối với véc-tơ. Các phép toán như cộng, trừ, nhân, chia, lũy thừa, …, sẽ tác động lên tất cả các phần tử trong ma trận theo thứ tự của các phần tử xuất hiện trên ma trận. Bạn đọc hãy quan sát kết quả của các ví dụ sau để hiểu cách R thực hiện các phép toán trên ma trận

Nhân ma trận \(M\) kích thước \(3 \times 4\) với một số

M<-matrix(1:12, nrow = 3, ncol = 4) 
M * 2 # in M*2 ra của sổ console
##      [,1] [,2] [,3] [,4]
## [1,]    2    8   14   20
## [2,]    4   10   16   22
## [3,]    6   12   18   24

Bạn đọc có thể thấy rằng kết quả nhận được là một ma trận có kích thước bằng với kích thước của ma trận \(M\) và mỗi phần tử bằng phần tử ở vị trí tương ứng của ma trận \(M\) nhân với 2.

Khi thực hiện phép nhân thông thường ma trận \(M\) với một ma trận \(M_1\) có cùng kích thước thì kết quả nhận được là một ma trận mà mỗi phần tử bằng tích của 2 phần tử ở vị trí tương ứng của \(M\)\(M_1\). R sẽ báo lỗi nếu thực hiện phép nhân thông thường giữa hai ma trận không có cùng kích thước.

M<-matrix(1:12, nrow = 3, ncol = 4) 
M1<-matrix(rep(c(0,1),6), nrow = 3, ncol = 4) 
M * M1# in M * M1 ra của sổ console
##      [,1] [,2] [,3] [,4]
## [1,]    0    4    0   10
## [2,]    2    0    8    0
## [3,]    0    6    0   12

Phép nhân thông thường cũng có thể được thực hiện giữa ma trận \(M\) với một véc-tơ có độ dài nhỏ hơn hoặc bằng số phần tử của \(M\). Trước khi thực hiệp phép nhân, R sẽ chuyển các phần tử trong véc-tơ vào một ma trận có kích thước tương ứng với \(M\) sau đó thực hiện phép nhân giống như nhân hai ma trận có cùng kích thước:

M<-matrix(1:12, nrow = 3, ncol = 4) 
x<-c(-2,-1,0,1,2) # véc-tơ độ dài 5
M * x # phép nhân được thực hiện mà không báo lỗi
##      [,1] [,2] [,3] [,4]
## [1,]   -2    4   -7   20
## [2,]   -2   10    0  -22
## [3,]    0  -12    9  -12

Khi thực hiện tính toán như trên, R đã tự động lặp lại \(x\) cho đến khi số lượng phần tử bằng với số phần tử của \(M\), điền các giá trị này vào một ma trận có kích thước bằng với kích thước của \(M\) rồi sau đó thực hiện phép nhân. Thật vậy

y<-rep(x,3) # lặp lại x cho đến khi số phần tử của véc-tơ thu được lớn hơn 12
M1<-matrix(y[1:12],nrow = 3, ncol = 4) # dùng 12 giá trị ban đầu để tạo thành ma trận có cùng kích thước 3*4
M * M1 # kết quả giống như M * x
##      [,1] [,2] [,3] [,4]
## [1,]   -2    4   -7   20
## [2,]   -2   10    0  -22
## [3,]    0  -12    9  -12

2.1.2 Cách lấy phần tử con và ma trận con của ma trận.

Tương tự như với véc-tơ, chúng ta sử dụng \([]\) để lấy phần tử con trong ma trận. Khác với véc-tơ, ma trận có chỉ số hàng và chỉ số cột nên chúng ta cần cho biết phần tử được lấy ra ở hàng thứ bao nhiêu và cột thứ bao nhiêu:

M[2,3] # phần tử ở hàng thứ hai, cột thứ ba của ma trận M
## [1] 8

Chúng ta cũng có thể lấy ra véc-tơ hàng hoặc véc-tơ cột của ma trận bằng cách sau

M[,3] # lấy ra véc-tơ cột thứ 3 của ma trận M
## [1] 7 8 9
M[2,] # lấy ra véc-tơ hàng thứ 2 của ma trận M
## [1]  2  5  8 11

Để lấy ra một ma trận con của một ma trận, chúng ta cũng tạo véc-tơ chỉ số giống như cách tạo chỉ số với véc-tơ. Thay vì chỉ tạo một véc-tơ chỉ số duy nhất như khi làm với véc-tơ, chúng ta cần tạo một véc-tơ chỉ số theo hàng và một véc-tơ chỉ số theo cột. Bạn đọc có thể tạo chỉ số bằng một véc-tơ kiểu số hoặc véc-tơ kiểu logical hoặc kết hợp cả hai phương pháp này

chi_so_hang<-c(TRUE,FALSE,TRUE) # Chỉ số theo hàng kiểu logical
chi_so_cot<-c(2,4) # chỉ số cột theo kiểu số
M[chi_so_hang,chi_so_cot] # ma trận con của ma trận M
##      [,1] [,2]
## [1,]    4   10
## [2,]    6   12

2.1.3 Các phép toán trên ma trận.

Các phép toán trên ma trận có ý nghĩa đặc biệt trong phân tích dữ liệu. Ở các chương sách tiếp theo bạn đọc sẽ thấy rằng tất cả các tính toán nhằm biến đổi dữ liệu, hoặc ước lượng tham số cho các mô hình trên dữ liệu đều dựa trên các phép tính toán trên ma trận. Chúng tôi sẽ giải thích các phép toán này một cách đơn giản nhất để những bạn đọc không có nền tảng chuyên sâu về toán cũng có thể hiểu được. Tuy nhiên, để có kỹ năng thành thạo trong biến đổi dữ liệu, phân tích dữ liệu, và xây dựng các mô hình trên dữ liệu, chúng tôi khuyên bạn đọc nên tự trang bị cho mình các kiến thức về đại số tuyến tính và giải tích véc-tơ.

2.1.3.1 Phép chuyển vị.

Phép toán chuyển vị (transpose) là một phép toán biến đổi một ma trận \(M\) kích thước \(n \times p\) (\(n\) hàng và \(p\) cột) thành một ma trận mới, ký hiệu là \(M^T\), có kích thước \(p \times n\) (\(p\) hàng và \(n\) cột), đồng thời phần tử hàng thứ \(j\) và cột thứ \(i\) của ma trận \(M^T\) bằng phần tử ở hàng thứ \(i\) và cột thứ \(j\) của ma trận \(M\).

\[\begin{align} \begin{bmatrix} m_{11} & m_{12} & m_{13}\\ m_{21} & m_{22} & m_{23} \end{bmatrix} \xrightarrow[\text{(transpose)}]{\text{chuyển vị}} \begin{bmatrix} m_{11} & m_{21} \\ m_{12} & m_{22} \\ m_{13} & m_{23} \end{bmatrix} \end{align}\]

Hàm số để thực hiện phép chuyển vị ma trận trong R là hàm t()

M<-matrix(1:12, nrow = 3, ncol = 4, byrow = TRUE) # nrow: số hàng, ncol: số cột
M # in M ra của sổ console
##      [,1] [,2] [,3] [,4]
## [1,]    1    2    3    4
## [2,]    5    6    7    8
## [3,]    9   10   11   12
t(M) # ma trận chuyển vị của ma trận 
##      [,1] [,2] [,3]
## [1,]    1    5    9
## [2,]    2    6   10
## [3,]    3    7   11
## [4,]    4    8   12

Bạn đọc có thể thấy rằng nếu thực hiện phép chuyển vị hai lần liên tiếp ta sẽ thu được ma trận ban đầu

t(t(M)) # ma trận chuyển vị của ma trận chuyển vị là ma trận ban đầu 
##      [,1] [,2] [,3] [,4]
## [1,]    1    2    3    4
## [2,]    5    6    7    8
## [3,]    9   10   11   12

2.1.4 Phép nhân ma trận

Phép nhân ma trận (matrix multiplication) của ma trận \(A\) với ma trận \(B\) chỉ thực hiện được nếu số cột của ma trận \(A\) bằng với số hàng của ma trận \(B\). Giả sử rằng \(A\) có kích thước là \(n \times p\)\(B\) có kích thước là \(p \times k\) thì kết quả của phép nhân ma trận của ma trận \(A\) với ma trận \(B\) là một ma trận \(M\) có kích thước \(n \times k\), phần tử ở hàng thứ \(i\) và cột thứ \(j\) của ma trận \(M\) là tích vô hướng giữa véc-tơ hàng \(i\) của ma trận \(A\) và véc-tơ cột \(j\) của ma trận \(B\). Nhắc lại với bạn đọc rằng tích vô hướng của hai véc-tơ \(x\)\(y\) (phải) có cùng độ dài \(n\) được ký hiệu là \(<x,y>\) và được tính như sau

\[\begin{align} <x,y> = \sum\limits_{i = 1}^n \ x_i y_i \end{align}\]

trong đó \(x_i\), \(y_i\) lần lượt là phần tử thứ \(i\) của véc-tơ \(x\) và véc-tơ \(y\). Để phân biệt với phép nhân thông thường, chúng tôi sử dụng ký hiệu \(*\) cho phép nhân ma trận mà chỉ đơn giản ký hiệu phép nhân ma trận giữa ma trận \(A\) và ma trận \(B\)\(AB\). Công thức dưới đây mô tả phép nhân ma trận giữa một ma trận có 2 hàng và 3 cột với một ma trận có 3 hàng và 4 cột để được một ma trận có kích thước là 2 hàng và 4 cột

\[\begin{align} && AB = M \\ && \begin{bmatrix} a_{11} & a_{12} & a_{13}\\ a_{21} & a_{22} & a_{23} \end{bmatrix} \% * \% \begin{bmatrix} b_{11} & b_{12} & b_{13} & b_{14} \\ b_{21} & b_{22} & b_{23} & b_{24} \\ b_{31} & b_{32} & b_{33} & b_{34} \end{bmatrix} = \begin{bmatrix} m_{11} & m_{12} & m_{13} & m_{14} \\ m_{21} & m_{22} & m_{23} & m_{24} \\ \end{bmatrix} \\ && m_{ij} = <A[i,],B[,j]> \end{align}\] trong đó \(A[i,]\) là véc-tơ hàng \(i\) của ma trận \(A\)\(B[,j]\) là véc-tơ cột \(j\) của ma trận \(B\).

Toán tử dùng để thực hiện phép nhân ma trận trong R là \(\% * \%\). Bạn đọc có thể thực hiện phép nhân hai ma trận \(A\)\(B\) như sau

A<-matrix(1:6, nrow = 2, ncol = 3) # ma trận kích thước 2 * 3
print(A)
##      [,1] [,2] [,3]
## [1,]    1    3    5
## [2,]    2    4    6
B<-matrix(1:12, nrow = 3, ncol = 4) # ma trận kích thước 3 * 4
print(B)
##      [,1] [,2] [,3] [,4]
## [1,]    1    4    7   10
## [2,]    2    5    8   11
## [3,]    3    6    9   12
M <- A %*% B # kết quả là ma trận M có kich thước 2 * 4
print(M)
##      [,1] [,2] [,3] [,4]
## [1,]   22   49   76  103
## [2,]   28   64  100  136

Chúng ta có thể kiểm tra giá trị của phần tử ở hàng thứ 2 và cột thứ 3 của ma trận kết quả (số 100) chính là tích vô hướng giữa véc-tơ hàng thứ hai của ma trận \(A\) và véc-tơ cột thứ ba của ma trận \(B\).

\[\begin{align} <(2,4,6),(7,8,9)> &=& 2 \times 7 + 4 \times 8 + 6 \times 9 \\ &=& 100 \end{align}\]

Bạn đọc cần phân biệt giữa phép nhân ma trận (ký hiệu \(\% * \%\)) và phép nhân thông thường (ký hiệu \(*\)) như đã trình bày ở trên. Để tránh gây nhầm lẫn, chúng tôi luôn sử dụng cụm từ nhân ma trận cho phép nhân \(\% * \%\).

2.1.5 Ma trận đường chéo và ma trận đơn vị.

Các khái niệm và phép toán trên ma trận được trình bày từ phần này sẽ chỉ áp dụng trên ma trận vuông, nghĩa là ma trận có số hàng bằng với số cột. Trong một ma trận vuông, các phần tử nằm trên đường chéo \(chính\) là các phần tử có chỉ số hàng bằng với chỉ số cột, các phần tử nằm trên đường chéo \(phụ\) là các phần tử có chỉ số hàng cộng với chỉ số cột bằng \((n+1)\) trong đó \(n\) là số hàng. Với ma trận vuông \(M\) có kích thước \(n \times n\) đường chéo chính của ma trận được ký hiệu là \(diag(M)\) xác định như sau

\[\begin{align} M = \begin{bmatrix} m_{11} & m_{12} & m_{13} & m_{14} \\ m_{21} & m_{22} & m_{23} & m_{24} \\ m_{31} & m_{32} & m_{33} & m_{34}\\ m_{41} & m_{42} & m_{43} & m_{44} \end{bmatrix} \\ diag(M) = (m_{11}, m_{22}, m_{33}, m_{44}) \end{align}\]

Ma trận có tất cả các phần tử nằm \(ngoài\) đường chéo chính bằng 0 được gọi là ma trận đường chéo. Hàm diag() trong R được sử dụng để lấy ra véc-tơ đường chéo chính của một ma trận vuông và để tạo ra một ma trận đường chéo hoặc cũng có thể dùng để khai báo một ma trận đường chéo.

M<-matrix(1:9, nrow = 3, ncol = 3) # ma trận vuông 3 * 3
print(M)
##      [,1] [,2] [,3]
## [1,]    1    4    7
## [2,]    2    5    8
## [3,]    3    6    9
diag(M) # lấy ra véc-tơ đường chéo chính của M
## [1] 1 5 9
M1<-diag(c(1,10,100)) # tạo ra ma trận đường chéo có đường chéo chính là (1,10,100)
print(M1)
##      [,1] [,2] [,3]
## [1,]    1    0    0
## [2,]    0   10    0
## [3,]    0    0  100

Ma trận đơn vị kích thước \(n \times n\), thường được ký hiệu \(I_n\), là một ma trận đường chéo mà tất cả các phần tử trên đường chéo chính bằng 1. Ma trận đơn vị \(I_n\) có tính chất quan trọng là mọi ma trận \(M\) kích thước \(k \times n\) khi thực hiện phép nhân ma trận với ma trận \(I_n\) sẽ được kết quả đúng bằng ma trận \(M\). Bạn đọc có thể quan sát ví dụ sau

In<-diag(rep(1,4)) # ma trận đơn vị kích thước 4*4
print(In)
##      [,1] [,2] [,3] [,4]
## [1,]    1    0    0    0
## [2,]    0    1    0    0
## [3,]    0    0    1    0
## [4,]    0    0    0    1
M<-matrix(1:20, nrow = 5, ncol = 4) # ma trận vuông 5 * 4
print(M %*% In) # kết quả vẫn là ma trận M
##      [,1] [,2] [,3] [,4]
## [1,]    1    6   11   16
## [2,]    2    7   12   17
## [3,]    3    8   13   18
## [4,]    4    9   14   19
## [5,]    5   10   15   20

2.1.6 Định thức của ma trận

Định thức của ma trận là một khái niệm toán học phức tạp. Phần lớn bạn đọc khi làm quen với khái niệm định thức trong môn học đại số tuyến tính sẽ được giới thiệu về công thức tính định thức của một ma trận, hoặc sử dụng định thức của ma trận để thực hiện tính toán như giải hệ phương trình thay vì thực sự hiểu khái niệm định thức được bắt đầu từ đâu. Định thức là một giá trị số thực đặc trưng của một ma trận vuông. Định thức cho biết nhiều tính chất quan trọng của ma trận đó, đồng thời định thức xuất hiện trong rất nhiều tính toán liên quan đến ma trận.

Hãy bắt đầu với một ma trận vuông \(M\) kích thước \(2 \times 2\) như sau: \[\begin{align} M = \begin{bmatrix} m_{11} & m_{12}\\ m_{21} & m_{22} \end{bmatrix} \end{align}\]

Định thức của ma trận \(M\) được ký hiệu là \(|M|\) hoặc \(det(M)\) được xác định bởi công thức \[\begin{align} det(M) = m_{11} \times m_{22} - m_{12} \times m_{21} \end{align}\]

Định thức của ma trận \(M\) có thể biểu diễn dưới dạng diện tích của hình bình hành tạo thành từ 4 điểm có tọa độ \((0,0)\), \((m_{11},m_{12})\), \((m_{21},m_{22})\), và \((m_{11}+m_{21}, m_{12}+m_{22})\). Thật vậy, giả sử hai ma trận \(M\)\(M_1\) có kích thước \(2 \times 2\); ma trận \(M_1\) nhận được bằng cách đổi vị trí 2 dòng của ma trận \(M\)

\[\begin{align} M = \begin{bmatrix} 2 & 1 \\ 1 & 3 \end{bmatrix} \text{ và } M_1 = \begin{bmatrix} 1 & 3 \\ 2 & 1 \end{bmatrix} \\ det(M) = 5 \text{ và } det(M_1) = -5 \end{align}\]

Chúng ta có thể biểu diễn định thức của \(M\)\(M_1\) qua diện tích của các hình bình hành như hình vẽ dưới đây:

Giá trị của định thức sẽ cho ta biết thông tin về các véc-tơ tạo nên ma trận:

  • Định thức bằng 0 chỉ xảy ra khi hai véc-tơ (hai mũi tên màu xanh và màu đỏ xuất phát từ điểm \((0,0)\)) trùng nhau hoặc đối đỉnh nhau. Điều này chỉ xảy ra khi véc-tơ thứ nhất bằng véc-tơ thứ hai nhân với một số. Trong trường hợp tổng quát với ma trận vuông kích thước \(n \times n\), định thức bằng 0 khi một véc-tơ nào đó là tổ hợp tuyến tính của các véc-tơ còn lại.

  • Định thức gần bằng 0 nghĩa là góc tạo bởi 2 véc-tơ rất gần 0 hoặc tạo với nhau một góc xấp xỉ 180 độ. Ma trận vuông kích thước \(n \times n\) có định thức xấp xỉ bằng 0 nghĩa là mối liên hệ tuyến tính giữa các véc-tơ của ma trận là rất chặt chẽ.

  • Dấu của định thức cho ta biết vị trí của các véc-tơ. Véc-tơ hàng thứ nhất tương ứng với màu xanh dương trong khi véc-tơ hàng thứ hai tương ứng với màu đỏ. Dấu của định thức dương chỉ khi véc-tơ màu xanh dương nằm phía trên (bên trái) véc-tơ màu đỏ, và dấu của định thức là âm chỉ khi véc-tơ màu xanh dương nằm phía dưới (bên phải) véc-tơ màu đỏ.

Với các ma trận vuông kích thước \(n \times n\); \(n \geq 3\) định thức của ma trận được tính bằng cách lựa chọn một dòng (hoặc cột) thứ \(i\) bất kỳ và sau đó thực hiện phép khai triển \[\begin{align} det(M) = \sum\limits_{j = 1}^n (-1)^{i+j} \times m_{ij} \times det(M_{-i,-j}) \end{align}\] trong đó \(M_{-i,-j}\) mà ma trận vuông kích thước \((n-1) \times (n-1)\) nhận được sau khi bỏ đi hàng thứ \(i\) và cột thứ \(j\) của ma trận \(M\).

Định thức của ma trận \(M\) kích thước \(3 \times 3\) có thể tính toán dựa trên định thức của các ma trận con và lựa chọn hàng \(i=2\) như sau

\[\begin{align} & & M = \begin{bmatrix} m_{11} & m_{12} & m_{13} \\ m_{21} & m_{22} & m_{23} \\ m_{31} & m_{32} & m_{33} \end{bmatrix} \\ & & det(M) = - m_{21} \times \begin{vmatrix} m_{12} & m_{13} \\ m_{32} & m_{33} \end{vmatrix} + m_{22} \times \begin{vmatrix} m_{11} & m_{13} \\ m_{31} & m_{33} \end{vmatrix} - m_{23} \times \begin{vmatrix} m_{11} & m_{12} \\ m_{31} & m_{32} \end{vmatrix} \end{align}\]

Hàm det() trong R được sử dụng để tính định thức của ma trận.

M<-matrix(c(2,1,1,3),nrow = 2,ncol = 2,byrow = TRUE)
det(M)
## [1] 5

Một vài tính chất quan trọng của định thức:

  • Định thức của một ma trận đường chéo bằng tích các phần tử nằm trên đường chéo chính của ma trận đó. Ma trận đường chéo là một trường hợp đặc biệt của ma trận tam giác. Ma trận tam giác trên là ma trận có tất cả các phần tử nằm phía dưới đường chéo chính nhận giá trị bằng 0. Tương tự, ma trận tâm giác dưới là ma trận có tất cả các phần tử nằm phía trên đường chéo chính nhận giá trị bằng 0. Các ma trận tam giác có tính chất như đã phát biểu ở trên: định thức của các ma trận này bằng tích các phần tử nằm trên đường chéo chính.
M<-matrix(1:16,nrow = 4,ncol = 4) 
M[lower.tri(M)]<-0 # cho các phần tử phía dưới đường chéo chính bằng 0
print(M) # ma trận M là ma trận tam giác trên
##      [,1] [,2] [,3] [,4]
## [1,]    1    5    9   13
## [2,]    0    6   10   14
## [3,]    0    0   11   15
## [4,]    0    0    0   16
print(c(det(M), prod(diag(M)))) # định thức của M bằng tích các số trên đường chéo chính
## [1] 1056 1056
  • Định thức của ma trận chuyển vị bằng định thức của ma trận ban đầu \[\begin{align} det(M) = det(M^T) \end{align}\]
M<-matrix(rnorm(16),nrow = 4,ncol = 4) 
print(c(det(M),det(t(M))))
## [1] -4.377982 -4.377982
  • Định thức của tích hai ma trận bằng tích của các định thức. \[\begin{align} det(A \% * \% B) = det(A) \times det(B) \end{align}\]
M<-matrix(rnorm(16),nrow = 4,ncol = 4) 
M1<-matrix(rnorm(16),nrow = 4,ncol = 4) 
print(c(det(M%*%M1),det(M)*det(M1))) 
## [1] 0.09626011 0.09626011

2.1.7 Ma trận nghịch đảo

Ma trận nghịch đảo của một ma trận vuông \(M\), thường được ký hiệu \(M^{-1}\), là ma trận vuông có cùng kích thước với ma trận \(M\) và thỏa mãn tính chất: phép nhân ma trận giữa ma trận \(M\) với ma trận nghịch đảo \(M^{-1}\) sẽ cho kết quả là một ma trận đơn vị. Không phải ma trận vuông nào cũng có ma trận nghịch đảo; chỉ có các ma trận có định thức khác là có ma trận nghịch đảo. Các ma trận có ma trận nghịch đảo được còn được gọi là các ma trận khả nghịch. Các ma trận khả nghịch luôn có một ma trận nghịch đảo duy nhất.

\[\begin{align} M \% * \% M^{-1} = I_n \end{align}\]

Hai lần lấy nghịch đảo liên tiếp với một ma trận khả nghịch sẽ quay trở lại ma trận ban đầu, hay nói một cách khác ma trận nghịch đảo của ma trận \(M^{-1}\) chính là ma trận \(M\) \[\begin{align} M^{-1} \% * \% M = I_n \end{align}\]

Phương pháp chung để tính toán ma trận nghịch đảo là dựa trên các ma trận liên hợp (adjugate matrix). Ma trận liên hợp của ma trận \(M\) được ký hiệu là \(adj(M)\) là ma trận vuông kích thước \(n \times n\) mà phần tử ở hàng thứ \(i\), cột thứ \(j\) được tính bằng \[\begin{align} adj(M)_{ij} = (-1)^{j+i} det(M_{-j,-i}) \end{align}\] Khi tính toán định thức của ma trận \(M\), chúng tôi đã sử dụng ký hiệu \(M_{-i,-j}\) cho ma trận vuông kích thước \((n-1) \times (n-1)\) nhận được sau khi bỏ đi hàng thứ \(i\) và cột thứ \(j\) của ma trận \(M\). Bạn đọc lưu ý rằng có sự thay đổi vị trí của \(i\)\(j\) trong vế phải của phương trình ở trên. Ma trận nghịch đảo \(M^{-1}\) được tính từ ma trận liên hợp như sau \[\begin{align} M^{-1} = \cfrac{1}{det(M)} adj(M)_{ij} \end{align}\]

Nhìn chung, để tính toán ma trận nghịch đảo của một ma trận kích thước \(n \times n\), chúng ta sẽ phải tính toán định thức của ma trận ban đầu và định thức của \(n^2\) ma trận vuông có kích thước \((n-1) \times (n-1)\). Với các ma trận vuông có kích thước lớn, việc tính toán sử dụng công thức như trên sẽ tốn nhiều thời gian và bộ nhớ. Có nhiều thuật toán để tính xấp xỉ ma trận nghịch đảo của một ma trận. Trình bày các thuật toán này ở đây là không cần thiết. R sử dụng hàm solve() để tính toán ma trận nghịch đảo.

M<-matrix(rnorm(16),nrow = 4,ncol = 4) 
M1<-solve(M) # ma trận M1 là ma trận nghịch đảo của ma trận M
M1 %*% M # tích của M1 với M là ma trận đơn vị
##               [,1]          [,2]          [,3]          [,4]
## [1,]  1.000000e+00 -1.387779e-16 -2.220446e-16 -1.040834e-16
## [2,] -2.081668e-17  1.000000e+00  0.000000e+00  1.387779e-17
## [3,] -6.938894e-18  1.110223e-16  1.000000e+00 -2.775558e-17
## [4,] -3.469447e-17  1.665335e-16  1.110223e-16  1.000000e+00
M %*% M1 # tích của M với M1 là ma trận đơn vị
##               [,1]          [,2]          [,3]          [,4]
## [1,]  1.000000e+00 -1.734723e-17 -3.053113e-16  3.122502e-17
## [2,] -5.551115e-17  1.000000e+00  5.551115e-17 -1.144917e-16
## [3,]  0.000000e+00  0.000000e+00  1.000000e+00  5.551115e-17
## [4,]  0.000000e+00  1.873501e-16  2.775558e-17  1.000000e+00

Các tính toán liên quan đến định thức cần nhớ

  1. Định thức của ma trận sau khi nhân tất cả các phần tử với một số \[\begin{align} det(\lambda M) = \lambda^n \times det(M) \end{align}\]

  2. Định thức của ma trận chuyển vị \(M^{-T}\) bằng định thức của ma trận \(M\) \[\begin{align} det(M^T) = det(M) \end{align}\]

  3. Tích của định thức của ma trận \(M^{-1}\) với định thức của ma trận \(M\) bằng 1. \[\begin{align} det(M^{-1}) \times det(M) = det(I_n) = 1 \end{align}\]

2.2 Mảng nhiều chiều

Ma trận lưu phần tử trong hai chiều mà chúng ta gọi là hàng và cột. Đa số dữ liệu kiểu bảng biểu truyền thống đều có thể biểu diễn dưới dạng ma trận. Tuy nhiên có những kiểu dữ liệu mà khi biểu diễn dưới dạng ma trận hai chiều là không dễ dàng và có thể gây nhầm lẫn cho người sử dụng. Có thể kể đến dữ liệu kiểu hình ảnh. Khi bạn đọc lưu một bức ảnh màu lên trên máy tính điện tử, bức ảnh sẽ được số hóa thành một mảng ba chiều, bao gồm có chiều cao, chiều rộng của ảnh và một chiều thứ ba là màu sắc của điểm ảnh. Phức tạp hơn nữa nếu dữ liệu là một đoạn phim, hay một hình động, bạn đọc sẽ cần phải sử dụng thêm chiều thứ tư để mô tả thời gian xuất hiện của mỗi hình ảnh trong đoạn phim.

Thông thường thì người làm việc với dữ liệu sẽ đổi các mảng nhiều chiều về mảng hai chiều hoặc một chiều (véc-tơ) để dễ dàng xử lý. Các thao tác cơ bản khi làm việc với mảng nhiều chiều là cần thiết để thực hiện xử lý dữ liệu một cách chính xác.

2.2.1 Khởi tạo mảng nhiều chiều

Để tạo mảng nhiều chiều bạn đọc sử dụng hàm \(array()\).

Ar<-array(1:24,dim=c(2,3,4)) # mảng 3 chiều
Ar # hiển thị mảng 3 chiều Ar
## , , 1
## 
##      [,1] [,2] [,3]
## [1,]    1    3    5
## [2,]    2    4    6
## 
## , , 2
## 
##      [,1] [,2] [,3]
## [1,]    7    9   11
## [2,]    8   10   12
## 
## , , 3
## 
##      [,1] [,2] [,3]
## [1,]   13   15   17
## [2,]   14   16   18
## 
## , , 4
## 
##      [,1] [,2] [,3]
## [1,]   19   21   23
## [2,]   20   22   24

Bạn đọc có thể thấy R hiển thị mảng ba chiều \(Ar\) kích thước \(2 \times 3 \times 4\) như là sự kết hợp của 4 ma trận kích thước \(2 \times 3\). Tương tự như khi khởi tạo giá trị cho ma trận, số lượng phần tử đưa vào trong mảng phải bằng với số phần tử của mảng, trong trường hợp mảng \(Ar\) ở trên là véc-tơ có độ dài 24 tương ứng với \(2 \times 3 \times 4 = 24\) phần tử của mảng.

Để lấy ra các phần tử con (một biến, một ma trận, hay 1 mảng nhiều chiều) từ một nhiều mảng, bạn đọc sử dụng \([]\) giống như khi làm với ma trận. Lưu ý rằng khi lấy phần tử con từ một mảng, bạn đọc cần phải sử dụng chỉ số cho tất cả các chiều.

Ar[1,2,1] # phần tử có các chỉ số 1 - 2 - 1 
## [1] 3
Ar[,,1] # ma trận 2 * 3
##      [,1] [,2] [,3]
## [1,]    1    3    5
## [2,]    2    4    6
Ar[,c(1,3),c(1,4)] # mảng trận 2 * 2 * 2
## , , 1
## 
##      [,1] [,2]
## [1,]    1    5
## [2,]    2    6
## 
## , , 2
## 
##      [,1] [,2]
## [1,]   19   23
## [2,]   20   24

Thứ tự các phần tử khi điền vào mảng khi sử dụng hàm trong hàm array() sẽ là ưu tiên ma trận kích thước \(2 \times 3\) tương ứng với chỉ số \([,,1]\) trước, rồi đến ma trận kích thước \(2 \times 3\) tương ứng với chỉ số \([,,2]\),…, và tiếp tục như thế cho đến khi tất cả các phần tử của mảng được gán giá trị.

2.2.2 Sử dụng mảng nhiều chiều để biến đổi dữ liệu kiểu hình ảnh

Để bạn đọc có cái nhìn trực quan hơn về mảng nhiều chiều, chúng ta sẽ thực hiện các phép biến đổi, tính toán trên một dữ liệu cụ thể. Như chúng tôi đã nói ở trên, mảng nhiều chiều là một đối tượng thích hợp dùng để lưu dữ liệu kiểu hình ảnh. Thư viện \(imager\) được cài thêm trên R có các hàm thích hợp để làm việc với dữ liệu kiểu hình ảnh. Chúng ta sẽ sử dụng một mảng nhiều chiều để lưu một bức ảnh và thực hiện các phép biến đổi bức ảnh đó sử dụng tính toán trên mảng nhiều chiều.

#install.packages("imager")
library(imager)

Hàm load.image() trong thư viện \(imager\) có thể đọc các file hình ảnh có định dạng \(png\), \(jpeg\), hoặc \(bmp\). Bạn đọc có thể đọc một hình ảnh có một trong các định dạng kể trên

setwd("../KHDL_KTKD/Image")
img<-load.image("cat.jpg") # đọc hình ảnh tên "cat" vào 
plot(img)

Để biết \(img\) là kiểu đối tượng nào, bạn đọc dùng hàm class()

class(img)
## [1] "cimg"         "imager_array" "numeric"

R cho biết đây là một đối tượng kiểu \(cimg\). Kiểu đối tượng này về bản chất là một mảng bốn chiều. Chiều thứ nhất là cho biết chiều rộng của bức ảnh, chiều thứ hai cho biết chiều cao của bức ảnh, chiều thứ ba luôn bằng 1 đối với dữ liệu kiểu ảnh và chiều thứ tư bằng 3 nếu bức ảnh là ảnh màu.

Đối tượng kiểu \(cimg\) cho phép bạn đọc thực hiện các biến đổi, tính toán giống như trên một mảng nhiều chiều mà không cần phải chuyển đổi sang kiểu mảng. Chẳng hạn như để biết bức ảnh được lưu bởi đối tượng tên \(img\) có bao nhiêu chiều, chúng ta sử dụng hàm dim() giống như với mảng

dim(img) # mảng bốn chiều: chiều rộng * chiều cao * chiều thời gian * chiều màu sắc
## [1] 535 595   1   3

Bức ảnh được lưu bởi đối tượng \(img\) ở trên là một bức ảnh màu có chiều rộng 535 và chiều cao 595. Chiều thứ ba bằng 1 nghĩa là đây là một bức ảnh (chiều thứ ba lớn hơn 1 khi đối tượng là hình ảnh động). Chiều thứ tư bằng 3 đại diện cho 3 màu đỏ (Red), màu xanh lá cây (Green), và màu xanh da trời (Blue). Bạn đọc có thể hình dung một bức ảnh màu ở trên như là sự kết hợp của ba ma trận cùng kích thước \(535 \times 595\), ma trận thứ nhất đại diện cho màu đỏ, ma trận thứ hai đại diện cho màu xanh lá cây và ma trận thứ ba đại diện cho màu xanh da trời. Mỗi giá trị trong ma trận là một số trong khoảng từ 0 đến 1. Giá trị 0 tương ứng với màu đen và giá trị càng gần 1 thì màu sắc của điểm đó càng gần màu mà ma trận đại diện. Để quan sát ma trận tương ứng với mỗi màu, bạn đọc cần gán giá trị của 2 ma trận còn lại bằng 0 trước khi hiển thị.

img_red<-img
img_red[,,1,2:3]<-0 # cho 2 ma trận màu xanh lá cây và xanh da trời bằng 0

img_green<-img
img_green[,,1,c(1,3)]<-0 # cho 2 ma trận màu đỏ và xanh da trời bằng 0

img_blue<-img
img_blue[,,1,1:2]<-0 # cho 2 ma trận màu xanh lá cây và đỏ bằng 0

par(mfrow = c(1,3))
plot(img_red, main = "Chỉ giữa lại màu đỏ")
plot(img_green, main = "Chỉ giữa lại màu xanh lá cây")
plot(img_blue, main = "Chỉ giữa lại màu xanh da trời")

Hàm as.cimg() được dùng để đổi một mảng bốn chiều sang kiểu \(cimg\) để có thể hiển thị khi sử dụng hàm plot(). Lưu ý rằng hãy luôn sử dụng chiều thứ ba bằng 1 và chiều thứ tư bằng 3 nếu bạn muốn tạo ảnh màu. Các câu lệnh dưới đây tạo ra các bức ảnh mà các giá trị trong các ma trận màu sắc hoàn toàn là các giá trị ngẫu nhiên phân phối đều (uniform) trong khoảng (0,1).

img1<-array(runif(5*5*1*3),dim=c(5,5,1,3)) # bức ảnh màu kích thước 5*5
img1<-as.cimg(img1)

img2<-array(runif(1000*1000*1*3),dim=c(1000,1000,1,3)) # bức ảnh màu kích thước 1000*1000
img2<-as.cimg(img2)

par(mfrow = c(1,2))
plot(img1, interpolate = FALSE, main = "Ảnh nhiễu kích thước 5 * 5") 
plot(img2, interpolate = FALSE, main = "Ảnh nhiễu kích thước 1000 * 1000")

Tham số \(interpolate\) nhận giá trị bằng \(FALSE\) có nghĩa là các điểm ảnh giữ nguyên giá trị. Tham số này có giá trị là mặc định là \(TRUE\). Khi \(interpolate\) nhận giá trị bằng \(TRUE\) hình ảnh hiển thị sẽ có sự giao thoa về màu sắc tại viền các điểm ảnh và làm cho ảnh nhìn mượt mà hơn.

Về bản chất, xử lý ảnh trên máy tính điện tử chính là xử lý các con số nằm trong mảng nhiều chiều. Chúng tôi sẽ giới thiệu một vài kỹ thuật xử lý đơn giản trên ảnh để bạn đọc có thể hiểu hơn về xử lý mảng nhiều chiều. Trước hết là thao tác cắt ảnh. Cắt ảnh chính là một phép lấy mảng con từ một mảng ban đầu. Thật vậy,

n<-dim(img)[1] # chiều rộng của ảnh
k<-round(n/2) # điểm giữa để chia ảnh làm hai nửa
img1<-img[1:k,,,] # img1 là nửa bên trái của ảnh
img2<-img[(k+1):n,,,] # img2 là nửa bên phải của ảnh
par(mfrow = c(1,2))
plot(as.cimg(img1), main = "Nửa bên trái") 
plot(as.cimg(img2), main = "Nửa bên phải")

Tiếp theo, chúng ta sẽ thực hiện tăng hoặc giảm độ sáng của ảnh. Tăng hoặc giảm độ sáng của ảnh tương đương với việc điều chỉnh đồng thời các số trong mảng nhiều chiều gần hơn đến giá trị 1 hoặc gần hơn đến giá trị 0. Chúng ta thực hiện như sau

img1<-img + (1 - img) * 0.3 # img1 là bức ảnh sau khi tăng độ sáng lên 30% 
img2<-img - img * 0.3 # img2 là bức ảnh sau khi giảm độ sáng đi 30%
par(mfrow = c(1,3))
plot(img, rescale = FALSE, main= "Ảnh ban đầu") 
plot(as.cimg(img1), rescale = FALSE, main = "Tăng độ sáng") 
plot(as.cimg(img2),rescale = FALSE, main = "Giảm độ sáng")

Một kỹ thuật xử lý ảnh khác là giảm kích thước của ảnh. Giả sử bạn đọc muốn giảm kích thước ảnh mỗi chiều 50%. Để làm được việc này, mỗi ma trận kích thước \(n \times m\) sẽ được đổi thành ma trận kích thước \([n/2] \times [m/2]\) trong đó \([n/2]\) là phần nguyên của số \(n/2\). Nguyên tắc chuyển từ ma trận ban đầu sang ma trận có kích thước nhỏ hơn là mỗi ô \(2 \times 2\) của ma trận ban đầu được chuyển thành 1 số trong ma trận mới

giamchieu<-function(M,k){
  n<-dim(M)[1]; m<-dim(M)[2]
  n1<-round(n/k)-1; m1<-round(m/k)-1
  M1<-matrix(0,n1,m1)
  for (i in 1:n1){
    for (j in 1:m1){
      i1<-(k*(i-1)+1):(k*i)
      j1<-(k*(j-1)+1):(k*j)
      M1[i,j]<-mean(M[i1,j1],na.rm=TRUE)
    }
  }
  return(M1)
}

n<-dim(img)[1]; m<-dim(img)[2]
k1<-10; k2<-20
n1<-round(n/k1)-1; m1<-round(m/k1)-1
n2<-round(n/k2)-1; m2<-round(m/k2)-1

img1<-array(0,dim=c(n1,m1,1,3));
img2<-array(0,dim=c(n2,m2,1,3));

for (i in 1:3){ 
  img1[,,1,i]<-giamchieu(img[,,1,i],k1)
  img2[,,1,i]<-giamchieu(img[,,1,i],k2)
}

par(mfrow = c(1,3))
plot(img, interpolate = FALSE, main= "Ảnh ban đầu") 
plot(as.cimg(img1), interpolate = FALSE, main = "Giảm kích thước 1:10") 
plot(as.cimg(img2), interpolate = FALSE, main = "Giảm kích thước 1:20")

2.3 List

Không giống như véc-tơ, ma trận, hay mảng nhiều chiều, \(list\) là một cấu trúc trong R mà có thể chứa nhiều kiểu đối tượng khác nhau bao gồm biến, véc-tơ, ma trận, và cả các \(list\) khác. Với những bạn đọc đã học qua Python, \(list\) cũng giống như một \(dictionary\). Đối với các bạn đọc đã học qua ngôn ngữ lập trình C++, \(list\) tương tự như một \(struct\). \(list\) đóng vai trò quan trọng trong R, đặc biệt là trong viết hàm số và lập trình hướng đối tượng.

Trong phần này của cuốn sách, chúng ta sẽ tìm hiểu cách tạo ra \(list\) và ứng dụng cấu trúc của \(list\) để phục vụ công việc phân tích dữ liệu một cách hiệu quả nhất.

2.3.1 Khởi tạo \(list\) và chỉ số của \(list\).

Hàm số để tạo ra một \(list\) trong R là hàm list(). Giả sử chúng ta muốn tạo thành một đối tượng có tên \(SV1\) chứa các thông tin về một sinh viên

  1. Tên của sinh viên: được lưu trong một biến kiểu chuỗi ký tự.

  2. Ngày sinh của sinh viên: được lưu trong một biến kiểu thời gian.

  3. Giới tính của sinh viên: được lưu trong một biến kiểu logic, giá trị \(TRUE\) tương ứng với giới tính Nam, và \(FALSE\) tương ứng với giới tính nữ.

  4. Bảng điểm của sinh viên: là một \(data.frame\) có 2 cột, 1 cột là tên môn học và một cột là điểm của môn học tương ứng.

Chúng ta sử dụng hàm list() để tạo ra một \(list\) có tên \(SV1\) như sau

SV1<-list(Ten = "Nguyễn Đức Nam",
          Ngay_sinh = as.Date("2000-06-20"),
          Gioi_tinh = TRUE,
          Bang_diem = data.frame(Mon_hoc = c("Giải tích", "Đại số", "Xác suất"),
                                 Diem = c(6.5, 8.5, 7.0)))
str(SV1) # xem cấu trúc của list SV1
## List of 4
##  $ Ten      : chr "Nguyễn Đức Nam"
##  $ Ngay_sinh: Date[1:1], format: "2000-06-20"
##  $ Gioi_tinh: logi TRUE
##  $ Bang_diem:'data.frame':   3 obs. of  2 variables:
##   ..$ Mon_hoc: chr [1:3] "Giải tích" "Đại số" "Xác suất"
##   ..$ Diem   : num [1:3] 6.5 8.5 7

Bạn đọc có thể thấy \(SV1\) có bốn đối tượng con có tên là \(Ten\), \(Ngay\_sinh\), \(Gioi\_tinh\), và \(Bang\_diem\). Mỗi đối tượng con có một kiểu giá trị khác nhau, riêng đối tượng con \(Bang\_diem\) là một dữ liệu (\(data.frame\)).

Để lấy ra một đối tượng con của \(list\) bạn đọc sử dụng ký hiệu \(\$\). Chẳng hạn bạn đọc muốn hiển thị bảng điểm của sinh viên có thông tin được lưu trong \(list\) SV1, bạn đọc sử dụng câu lệnh sau

SV1$Bang_diem # hiển thị bảng điểm
##     Mon_hoc Diem
## 1 Giải tích  6.5
## 2    Đại số  8.5
## 3  Xác suất  7.0

Để biết tên các đối tượng trong một \(list\), bạn đọc sử dụng hàm names()

names(SV1)
## [1] "Ten"       "Ngay_sinh" "Gioi_tinh" "Bang_diem"

Một cách khác để lấy ra một đối tượng con của \(list\) là sử dụng chỉ số của đối tượng. Do bảng điểm nằm ở vị trí thứ 4 trong list nên bạn đọc sử dụng câu lệnh sau

SV1[[4]] # sử dụng 2 lần dấu ngoặc vuông
##     Mon_hoc Diem
## 1 Giải tích  6.5
## 2    Đại số  8.5
## 3  Xác suất  7.0

Bạn đọc có thể thấy rằng để lấy ra phần tử con, chúng ta cần phải sử dụng hai lần dấu ngoặc vuông. Nếu chỉ sử dụng một dấu ngoặc vuông, phần tử được lấy ra sẽ là 1 list có 1 phần tử và phần tử duy nhất là bảng điểm.

SV1[4] # là một list có 1 phần tử
## $Bang_diem
##     Mon_hoc Diem
## 1 Giải tích  6.5
## 2    Đại số  8.5
## 3  Xác suất  7.0

Để thêm một đối tượng vào \(list\), chúng ta có thể đặt tên trực tiếp cho đối tượng mới và gán giá trị cho đối tượng. Ví dụ như chúng ta muốn thêm thông tin về quê quán của sinh viên vào một đối tượng có tên là \(que\_quan\)

SV1$que_quan<-"Hà Nội" # Thêm vào một phần tử có tên que_quan là một biến
str(SV1) # list SV1 đã có thêm phần tử thứ năm
## List of 5
##  $ Ten      : chr "Nguyễn Đức Nam"
##  $ Ngay_sinh: Date[1:1], format: "2000-06-20"
##  $ Gioi_tinh: logi TRUE
##  $ Bang_diem:'data.frame':   3 obs. of  2 variables:
##   ..$ Mon_hoc: chr [1:3] "Giải tích" "Đại số" "Xác suất"
##   ..$ Diem   : num [1:3] 6.5 8.5 7
##  $ que_quan : chr "Hà Nội"

Để xóa đi một đối tượng khỏi list, chúng ta gán cho đối tượng đó giá trị bằng \(NULL\)

SV1$que_quan<-NULL # xóa phần tử có tên que_quan khỏi SV1
str(SV1) # list SV1 chỉ còn 4 phần tử
## List of 4
##  $ Ten      : chr "Nguyễn Đức Nam"
##  $ Ngay_sinh: Date[1:1], format: "2000-06-20"
##  $ Gioi_tinh: logi TRUE
##  $ Bang_diem:'data.frame':   3 obs. of  2 variables:
##   ..$ Mon_hoc: chr [1:3] "Giải tích" "Đại số" "Xác suất"
##   ..$ Diem   : num [1:3] 6.5 8.5 7

Như chúng ta đã thảo luận trong phần giới thiệu, \(list\) là một cấu trúc nhiều lớp, nghĩa là một list có thể chứa các đối tượng có kiểu \(list\). Thật vậy, giả sử chúng ta có list \(SV2\) chứa các thông tin tương ứng của một sinh viên khác

SV2<-list(Ten = "Nguyễn Thị Loan",
          Ngay_sinh = as.Date("2000-05-13"),
          Gioi_tinh = FALSE,
          Bang_diem = data.frame(Mon_hoc = c("Xác suất", "Thống kê", "Học máy","AI"),
                                 Diem = c(7.0, 9.5, 10.0, 9.0)),
          Que_quan = "Hà Nội")

Chúng ta có thể tạo một \(list\) có tên là \(DS\) chứa thông tin của cả 2 sinh viên

DS<-list(SV1 = SV1,SV2 = SV2) # DS là một list có 2 phần tử, mỗi phần tử là 1 list
str(DS) # xem cấu trúc của list DS
## List of 2
##  $ SV1:List of 4
##   ..$ Ten      : chr "Nguyễn Đức Nam"
##   ..$ Ngay_sinh: Date[1:1], format: "2000-06-20"
##   ..$ Gioi_tinh: logi TRUE
##   ..$ Bang_diem:'data.frame':    3 obs. of  2 variables:
##   .. ..$ Mon_hoc: chr [1:3] "Giải tích" "Đại số" "Xác suất"
##   .. ..$ Diem   : num [1:3] 6.5 8.5 7
##  $ SV2:List of 5
##   ..$ Ten      : chr "Nguyễn Thị Loan"
##   ..$ Ngay_sinh: Date[1:1], format: "2000-05-13"
##   ..$ Gioi_tinh: logi FALSE
##   ..$ Bang_diem:'data.frame':    4 obs. of  2 variables:
##   .. ..$ Mon_hoc: chr [1:4] "Xác suất" "Thống kê" "Học máy" "AI"
##   .. ..$ Diem   : num [1:4] 7 9.5 10 9
##   ..$ Que_quan : chr "Hà Nội"

Để xem bảng điểm của sinh viên thứ hai, chúng ta cần sử dụng 2 lần ký hiệu \(\$\):

DS$SV2$Bang_diem # Xem bảng điểm của sinh viên thứ hai
##    Mon_hoc Diem
## 1 Xác suất  7.0
## 2 Thống kê  9.5
## 3  Học máy 10.0
## 4       AI  9.0

2.3.2 Sử dụng \(list\) với hàm số

Hầu như tất cả các hàm số được xây dựng sẵn trong R đều cho kết quả đầu ra dưới dạng \(list\). Bạn đọc quan sát giá trị đầu ra của hàm có tên là uniroot() như sau

f<-function(x) x^2 - 1/4
result<-uniroot(f,c(0,1))
class(result) # đối tượng result có kiểu list
## [1] "list"
str(result) # xem cấu trúc của đối tượng result
## List of 5
##  $ root      : num 0.5
##  $ f.root    : num -2.85e-05
##  $ iter      : int 6
##  $ init.it   : int NA
##  $ estim.prec: num 6.1e-05

Hàm uniroot() được sử dụng để tìm nghiệm duy nhất của một hàm số trên một khoảng. Đoạn câu lệnh ở trên sử dụng hàm \(uniroot()\) để tìm nghiệm duy nhất của phương trình \(x^2 - 1/4 = 0\) trên khoảng \((0,1)\). Kết quả của hàm uniroot() là một list có 5 phần tử đều là các biến kiểu số với tên tương ứng là \(root\), \(f.root\), \(iter\), \(init.it\), và $ estim.prec$. Các hàm số phức tạp hơn sẽ có cấu trúc của kết quả đầu ra phức tạp hơn rất nhiều. Bạn đọc cần đọc kỹ hướng dẫn của các hàm để hiểu mỗi đối tượng con của kết quả đầu ra có ý nghĩa như thế nào.

Bạn đọc cũng nên sử dụng kiểu \(list\) để làm đầu ra cho các hàm số tự xây dựng. Chúng ta sẽ quay trở lại ví dụ về xây dựng hàm số \(PV\) để tính giá trị hiện tại của một dòng tiền. Đối với một dòng tiền tương lai, ngoài giá trị hiện tại, bạn đọc có thể quan tâm đến các giá trị khác như Macaulay Duration, Modified Duration, Dollar Duration, Convexity (ý nghĩa và cách tính các giá trị này ở trong phần phụ lục của chương này).

summaryCF<-function(i,CF){
  n<-length(CF)
  PV<-sum(CF/((1+i)^(1:n)))
  Mac_D<-sum(CF*(1:n)/((1+i)^(1:n)))/PV
  Mod_D<-Mac_D/(1+i)
  Dollar_D<-PV*Mod_D*100
  Convexity<-Mod_D
  ket_qua<-list(PV = PV, Mac_D = Mac_D, Mod_D = Mod_D, 
               Dollar_D = Dollar_D, Convexity = Convexity)
  return(ket_qua)
}

Chúng ta có thể sử dụng hàm summary_CF() để tính toán các đặc trưng của một trái phiếu với các thông số như sau:

(#tab:unnamed-chunk-174)Các thông số của một trái phiếu
Thông số trái phiếu Giá trị
Ngày hiện tại Ngày 01 tháng 10 năm 2023
Ngày đáo hạn Ngày 30 tháng 09 năm 2035
Mệnh giá 10 tỷ Vnd
Lãi suất Coupon 9,25%
Số lần trả coupon trong năm 1 lần/năm
Lãi suất chiết khấu 5,00%

Chúng ta tạo ra dòng tiền tương lai của trái phiếu với các thông số như trên và sau đó sử dụng hàm summary_CF()

# Nhập liệu
F<-10 # mệnh giá trái phiếu, đơn vị tỷ đồng 
T<-12 # 12 cho đến ngày đáo hạn
c<-9.25/100 # lãi suất coupon
i<-5/100 # lãi suất dùng để chiết khấu
CF<-c(rep(c*F,(T-2)),c*F+F) # Dòng tiền tương lai của trái phiếu
summaryCF(i,CF)
## $PV
## [1] 13.53023
## 
## $Mac_D
## [1] 7.884908
## 
## $Mod_D
## [1] 7.509436
## 
## $Dollar_D
## [1] 10160.44
## 
## $Convexity
## [1] 7.509436

Chuyển sang một ví dụ khác khi sử dụng \(list\) làm đầu ra cho một hàm số tự xây dựng. Khi bạn đọc tìm hiểu về giá trị của một véc-tơ kiểu số, chúng ta thường tính toán các giá trị đặc trưng như giá trị trung bình, giá trị lớn nhất, nhỏ nhất, các phân vị, và muốn xem phân phối các giá trị trong véc-tơ đó như thế nào. Chúng ta có thể tự viết một hàm số để thực hiện việc này với đầu ra là một \(list\)

summary_vec<-function(x){
  do_dai<-length(x) # độ dài của véc-tơ
  ty_le_na<-paste(round(sum(is.na(x))/do_dai*100,2),"%") # % giá trị không quan sát được
  gioi_han<-c(min(x,na.rm=TRUE),max(x,na.rm=TRUE)) # lớn nhất và nhỏ nhất
  trung_binh<-mean(x,na.rm=TRUE)
  do_lech_chuan<-sd(x,na.rm=TRUE)
  phan_vi<-quantile(x,c(0.01,0.1,0.25,0.5,0.75,0.9,0.99),na.rm=TRUE)
  do_thi<-ggplot(data=data.frame(x=x), aes(x=x))+
    geom_histogram(color = "black", fill = "blue",alpha = 0.5)+
    xlab("")+ylab("")
  result<-list(do_dai = do_dai, ty_le_na = ty_le_na, gioi_han = gioi_han,
               trung_binh = trung_binh, do_lech_chuan = do_lech_chuan, 
               phan_vi = phan_vi, do_thi = do_thi)
  return(result)
}

Chúng ta có thể sử dụng hàm summary_vec() để tổng hợp thông tin về lợi suất tính theo ngày của chỉ số FTSE (chỉ số cổ phiếu của 100 công ty có giá trị vốn hóa thị trường lớn nhất niêm yết trên Sở giao dịch chứng khoán London) trong năm 1991 đến năm 1999. Chỉ số này được lưu trong dữ liệu \(EuStockMarkets\) có sẵn trong R.

chi_so<-EuStockMarkets[,4] # lấy chỉ số FTSE ra từ cột thứ 4 của EuStockMarkets
n<-length(chi_so) # độ dài của chuỗi chỉ số chứng khoán
loi_suat<-log(chi_so[2:n]/chi_so[1:(n-1)]) # lợi suất của chỉ số
summary_vec(loi_suat)
## $do_dai
## [1] 1859
## 
## $ty_le_na
## [1] "0 %"
## 
## $gioi_han
## [1] -0.04139903  0.05439552
## 
## $trung_binh
## [1] 0.0004319851
## 
## $do_lech_chuan
## [1] 0.007957728
## 
## $phan_vi
##            1%           10%           25%           50%           75% 
## -2.060655e-02 -9.139666e-03 -4.318778e-03  8.021069e-05  5.253592e-03 
##           90%           99% 
##  9.714781e-03  1.931723e-02 
## 
## $do_thi

Một lợi thế khác của đối tượng kiểu \(list\) là đẩy nhanh tốc độ tính toán khi dùng các hàm họ apply(). Chúng ta sẽ thảo luận vấn đề này trong phần tiếp theo của cuốn sách.

2.4 Các hàm họ apply()

Nhóm hàm apply() là nhóm hàm có sẵn trong R cho phép bạn đọc thực hiện lặp đi lặp lại một hàm số trên nhiều đối tượng. Về cơ bản nhóm hàm này hoạt động giống như một vòng lặp nhưng câu lệnh viết bằng nhóm hàm này sẽ chạy nhanh hơn và đơn giản hơn viết vòng lặp rất nhiểu.

Các hàm mà chúng tôi sẽ giới thiệu đến bạn đọc trong phần này bao gồm apply(), lapply()sapply. Còn nhiều hàm khác thuộc nhóm hàm này như vapply(), tapply(), mapply(), …, nhưng về nguyên tắc hoạt động của các hàm này là tương tự và chỉ khác ở chỗ chúng áp dụng trên các loại đối tượng khác nhau nên bạn đọc có thể tự tìm hiểu mà không gặp khó khăn nào.

2.4.1 Hàm apply()

Cho một véc-tơ \(x\) kiểu số và một hàm \(f\), chẳng hạn như \(f(x) = x^2\). Khi bạn đọc viết f(x) R sẽ hiểu rằng bạn đang thực hiện hàm số \(f\) cho từng phần tử của véc-tơ \(x\) và sẽ trả lại giá trị là một véc-tơ mà từng phần tử tương ứng là bình phương của các phần tử trong \(x\). Việc thực hiện hàm \(f\) trên véc-tơ \(x\) diễn ra một cách đồng thời và hiệu quả hơn so với việc viết một vòng lặp để tính hàm \(f\) trên từng phần tử của \(x\).

x<-1:5; f<-function(x) x^2
f(x) # f được áp dụng trên từng phần tử của x
## [1]  1  4  9 16 25

Điều gì xảy ra khi \(x\) không phải là một véc-tơ đồng các phần tử con của \(x\) không phải là một biến, chẳng hạn như

  1. \(x\) là một ma trận và bạn muốn tính toán một hàm \(f\) trên các phần tử con của \(x\) là một véc-tơ hàng hoặc một véc-tơ cột.

  2. \(x\) là một dữ liệu và bạn muốn thực hiện một hàm \(f\) trên tất cả các cột dữ liệu.

  3. \(x\) là một list và bạn muốn thực hiện một hàm \(f\) trên tất cả các đối tượng con của \(x\).

Các hàm thuộc họ apply() sẽ giúp bạn đọc thực hiện tác tính toán như vậy. Cách viết hàm apply() như sau:

apply(x, MARGIN, FUN, ...)

trong đó \(x\) là một ma trận, một mảng nhiều chiều, hoặc một dữ liệu; \(MARGIN\) là một số, hoặc véc-tơ chỉ số cho biết hàm sẽ áp dụng trên chiều (hoặc các chiều) nào, và \(FUN\) là hàm số mà bạn muốn thực hiện. Ví dụ như bạn đọc muốn tính giá trị trung bình của mỗi cột của một ma trận \(M\), hãy sử dụng câu lệnh như sau

M<-matrix(1:100,20,5) # ma trận kích thước 20 * 5
apply(M, MARGIN = 2, FUN = mean) # MARGIN = 2 nghĩa là áp dụng theo cột (1 nghĩa là theo hàng)  
## [1] 10.5 30.5 50.5 70.5 90.5

Do ma trận \(M\) có 5 cột nên giá trị trả lại là một véc-tơ kiểu số có độ dài bằng 5. Véc-tơ này chứa giá trị là trung bình của các cột thứ tự 1, 2, 3, 4, và 5 của ma trận \(M\).

Về nguyên tắc đối tượng sử dụng trong hàm apply() là ma trận hoặc mảng nhiều chiều. Bạn đọc cũng có thể sử dụng hàm apply() trên đối tượng là dữ liệu (data.frame). Khi đối tượng của hàm apply() có từ 3 chiều trở lên, giá trị của tùy biến \(MARGIN\) có thể là một số hoặc một véc-tơ. Thật vậy,

Ar<-array(1:20,dim=c(5,2,2)) # mảng kích thước 5 * 2 * 2
apply(Ar, MARGIN = 3, FUN = mean) # MARGIN = 3 nghĩa là áp dụng hàm mean theo chiều thứ 3  
## [1]  5.5 15.5

Giá trị trả lại sẽ là một véc-tơ có độ dài là 2, phần tử thứ nhất là giá trị trung bình của các phần tử thuộc ma trận kích thước \(5 \times 2\) thứ nhất (ma trận \(Ar[,,1]\)) và phần tử thứ hai là giá trị trung bình của các phần tử thuộc ma trận kích thước \(5 \times 2\) thứ hai (ma trận \(Ar[,,2]\)). Chúng ta có thể kiểm tra kết quả như sau:

mean(Ar[,,1]) # bằng phần tử thứ nhất khi dùng apply
## [1] 5.5
mean(Ar[,,2]) # bằng phần tử thứ hai khi dùng apply
## [1] 15.5

Chúng ta có thể áp dụng đồng thời hàm mean() theo chiều thứ 2 và chiều thứ 3 trên mảng \(Ar\) như sau

apply(Ar, MARGIN = c(2,3), mean) # MARGIN = c(2,3) nghĩa là áp dụng hàm mean theo chiều thứ 2 và 3  
##      [,1] [,2]
## [1,]    3   13
## [2,]    8   18

Kết quả thu được sẽ là một ma trận kích thước \(2 \times 2\) mà các phần tử sẽ tương ứng với giá trị trung bình:

  • Phần tử ở vị trí [1,1] của ma trận kết quả là giá trị trung bình của véc-tơ \(Ar[,1,1]\)

  • Phần tử ở vị trí [1,2] của ma trận kết quả là giá trị trung bình của véc-tơ \(Ar[,1,2]\)

  • Phần tử ở vị trí [2,1] của ma trận kết quả là giá trị trung bình của véc-tơ \(Ar[,2,1]\)

  • Phần tử ở vị trí [2,2] của ma trận kết quả là giá trị trung bình của véc-tơ \(Ar[,2,2]\)

Chúng ta có thể so sánh giá trị trung bình của các véc-tơ với ma trận kết quả của hàm apply():

mean(Ar[,1,1]) # bằng phần tử ở vị trí [1,1] của ma trận kết quả
## [1] 3
mean(Ar[,1,2]) # bằng phần tử ở vị trí [1,2] của ma trận kết quả
## [1] 13
mean(Ar[,2,1]) # bằng phần tử ở vị trí [1,2] của ma trận kết quả
## [1] 8
mean(Ar[,2,2]) # bằng phần tử ở vị trí [1,2] của ma trận kết quả
## [1] 18

Hàm số sử dụng với tùy biến \(FUN\) có thể là hàm số có sẵn trong R và các thư viện cài đặt bổ sung, hoặc cũng có thể là một hàm số mà bạn đọc tự xây dựng. Khi các câu lệnh của hàm số tự xây dựng ngắn gọn, bạn đọc có thể định nghĩa hàm số đó bên trong hàm apply(). Giá trị đầu ra của hàm số được tự định nghĩa cũng có thể là một véc-tơ, thậm chí là một ma trận hay là một mảng nhiều chiều, thậm chí là một \(list\). Nếu kết quả đầu ra của hàm sử dụng trong apply() là một ma trận hoặc mảng nhiều chiều, R sẽ chuyển ma trận hoặc mảng nhiều chiều về dạng véc-tơ. Trong trường hợp đầu ra của hàm tự định nghĩa là một \(list\), giá trị đầu ra sẽ là một ma trận hoặc mảng nhiều chiều mà mỗi phần tử con là một list. Dưới đây là một ví dụ mà giá trị đầu ra là một véc-tơ ba chiều.

apply(M, 2, function(x) c(min(x),mean(x),max(x))) 
##      [,1] [,2] [,3] [,4]  [,5]
## [1,]  1.0 21.0 41.0 61.0  81.0
## [2,] 10.5 30.5 50.5 70.5  90.5
## [3,] 20.0 40.0 60.0 80.0 100.0

Kết quả nhận được sẽ là một ma trận kích thước \(3 \times 5\). Cột thứ nhất của ma trận kết quả nhận giá trị (1, 10.5, 20) tương ứng với các giá trị \(min\), \(mean\), và \(max\) của véc-tơ \(M[,1]\).

M[,1]
##  [1]  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20
c(min(M[,1]), mean(M[,1]), max(M[,1]))
## [1]  1.0 10.5 20.0

Bạn đọc cũng có thể tự xây dựng hàm số trên môi trường chung sau đó gọi tên hàm số này trong hàm apply(). Nếu hàm số tự xây dựng là hàm số có tham số khác ngoài \(x\), bạn đọc cần phải khai báo giá trị cho tham số đó trong môi trường cục bộ của hàm apply():

my_range<-function(x, a) (max(x^a) - min(x^a)) # hàm số có tham số khác là a
apply(M, 2, my_range, a = 2) # cần khai báo giá trị tham số a của my_range trong hàm apply
## [1]  399 1159 1919 2679 3439

Khi tham số của hàm số mà bạn đọc muốn áp dụng trên ma trận là không cố định mà thay đổi theo một chiều của \(x\) thì không nên khai báo giá trị của tham số theo dạng véc-tơ trong hàm apply(). Thật vậy, giả sử bạn đọc muốn tính các giá trị phân vị ở mức xác suất 10% và 90% lần lượt của véc-tơ hàng thứ nhất và véc-tơ hàng thứ hai của một ma trận M kích thước \(2 \times 10\). Hàm số quantile(x, probs = p) là hàm số có sẵn trong R được sử dụng để tính giá trị phân vị tại mức xác suất \(p\) của véc-tơ số \(x\). Hãy quan sát kết quả của hàm apply() khi sử dụng tham số \(probs\) của hàm quantile() dưới dạng véc-tơ:

M<-matrix(1:20,2,10) # ma trận 2 * 10
apply(M, 1, quantile, probs = c(0.1,0.9)) # MARGIN = 1 nghĩa là tính theo các hàng của M
##     [,1] [,2]
## 10%  2.8  3.8
## 90% 17.2 18.2
quantile(M[1,],0.1) # giá trị mong muốn
## 10% 
## 2.8
quantile(M[2,],0.9) # giá trị mong muốn
##  90% 
## 18.2

Bạn đọc có thể thấy rằng kết quả của hàm apply() khi tham số \(probs\) là một véc-tơ là một ma trận, trong đó cột thứ nhất là giá trị phân vị tại các mức xác suất 10% và 90% của véc-tơ \(M[1,]\) và cột thứ hai giá trị phân vị tại các mức xác suất 10% và 90% của véc-tơ \(M[2,]\). Giá trị chúng ta mong muốn lấy ra chính là các số nằm trên đường chéo chính của ma trận kết quả. Sẽ không có khó khăn gì nếu số lượng hàng của ma trận \(M\) nhỏ. Bạn đọc sẽ gặp vấn đề khi số lượng véc-tơ được áp dụng là lớn bởi kích thước của ma trận kết quả sẽ tăng lên theo cấp số nhân. Thật vậy, nếu \(M\)\(n\) hàng và hàm số được áp dụng có 1 tham số, ma trận kết quả sẽ có kích thước sẽ là $ n n$ nếu bạn đọc sử dụng trực tiếp hàm apply(). Chẳng hạn như bạn muốn tính giá trị phân vị ở các mức xác suất 10%, 30%, 50%, 70%, và 90% của lần lượt các véc-tơ hàng thứ 1, 2, 3, 4, và 5 của một ma trận \(M\) kích thước \(5 \times 10\).

M<-matrix(1:50,5,10) # ma trận 5 * 10
apply(M, 1, quantile, probs = c(0.1,0.3,0.5,0.7,0.9)) # ma trận kết quả kích thước 5 * 5
##     [,1] [,2] [,3] [,4] [,5]
## 10%  5.5  6.5  7.5  8.5  9.5
## 30% 14.5 15.5 16.5 17.5 18.5
## 50% 23.5 24.5 25.5 26.5 27.5
## 70% 32.5 33.5 34.5 35.5 36.5
## 90% 41.5 42.5 43.5 44.5 45.5
diag(apply(M, 1, quantile, probs = c(0.1,0.3,0.5,0.7,0.9))) # lấy ra đường chéo chính
## [1]  5.5 15.5 25.5 35.5 45.5

Điều gì xảy ra khi ma trận \(M\)\(10^4\) véc-tơ hàng? Ma trận kết quả sẽ có kích thước là \(10^4 \times 10^4\). Khi ma trận \(M\)\(10^5\) véc-tơ hàng? Ma trận kết quả sẽ có kích thước là \(10^5 \times 10^5\) và R sẽ báo lỗi vì bộ nhớ không đủ để lưu một ma trận có kích thước như vậy.

Một cách đơn giản để tiết kiệm thời gian và bộ nhớ khi áp dụng hàm số có tham số là hãy thêm tham số vào như là một phần của ma trận \(M\) và điều chỉnh lại hàm quantile() trước khi sử dụng hàm apply()

M1<-cbind(M,c(0.1,0.3,0.5,0.7,0.9)) # thêm tham số vào cột cuối của ma trận M
my_quantile<-function(x){ # định nghĩa lại hàm quantile mà tham số p là giá trị cuối cùng trong véc-tơ
  n<-length(x)
  quantile(x[1:(n-1)], x[n])
}
apply(M1, 1, my_quantile) # áp dụng hàm mới trên ma trận mới
## [1]  5.5 15.5 25.5 35.5 45.5

2.4.2 Hàm lapply()sapply().

Cơ chế hoạt động của lapply() tương tự như apply() và chỉ khác ở đối tượng áp dụng và cấu trúc của kết quả đầu ra. lapply() thường áp dụng trên các đối tượng kiểu \(list\) hoặc các kiểu đối tượng mà có thể sử dụng ký hiệu \(\$\) để gọi phần tử con chẳng hạn như \(data.frame\) hoặc \(tibbles\). Chúng ta sẽ thảo luận về các đối tượng này ở phần sau của cuốn sách. Khi bạn đọc sử dụng hàm lapply(), bạn không cần phải sử dụng tùy biến \(MARGIN\) bởi vì lapply() sẽ luôn luôn hiểu các đối tượng được tác động đến là tất cả các đối tượng con của \(list\).

x <- list(x1 = 1:10, 
          x2 = c(TRUE,FALSE,TRUE,TRUE), 
          x3 = matrix(1:6,2,3), 
          x4 = list(x41 = c(1,2), x42 = c(3,4)) )
lapply(x, mean) # áp dụng hàm mean trên tất cả các phần tử con của x
## $x1
## [1] 5.5
## 
## $x2
## [1] 0.75
## 
## $x3
## [1] 3.5
## 
## $x4
## [1] NA

Kết quả của lapply() là một list có số lượng phần tử và tên các phần tử con giống với véc-tơ \(x\). Trong trường hợp áp dụng hàm mean(), mỗi giá trị nằm trong \(list\) kết quả là giá trị trung bình của phần tử có tên tương ứng nằm trong \(list\) ban đầu. Do hàm mean() không thể sử dụng với một \(list\) nên phần tử \(x4\) của kết quả nhận giá trị \(NA\).

Hàm sapply() có cơ chế hoạt động hoàn toàn tương tự như lapply() và chỉ khác ở chỗ kết quả đầu ra là dưới dạng véc-tơ, ma trận, hoặc mảng. Thật vậy, vẫn với đối tượng \(x\) kiểu \(list\) ở trên, chúng ta sử dụng sapply() thay vì lapply() sẽ cho kết quả dưới dạng véc-tơ thay vì dưới dạng \(list\)

sapply(x, mean) # áp dụng hàm mean trên tất cả các phần tử con của x
##   x1   x2   x3   x4 
## 5.50 0.75 3.50   NA

Các hàm lapply()sapply() thường xuyên được sử dụng khi làm việc với dữ liệu vì R lưu dữ liệu dưới dạng các \(data.frame\) hoặc \(tibbles\). Khi sử dụng các hàm lapply()sapply() với dữ liệu, các đối tượng được tác động đến sẽ luôn luôn là các véc-tơ cột. Hãy quan sát ví dụ dưới đây khi sử dụng sapply() để tính tỷ lệ giá trị không quan sát được của mỗi véc-tơ cột của một dữ liệu có tên là \(gapminder\) nằm trong thư viện \(dslabs\).

library(dslabs)
s1<-sapply(gapminder, function(x) sum(is.na(x))/length(x)) # tỷ lệ không quan sát được của mỗi cột trong dữ liệu
s1<-sort(s1) # sắp xếp s1 theo thứ tự tăng dần
print(s1) # hiển thị s1
##          country             year  life_expectancy        continent 
##       0.00000000       0.00000000       0.00000000       0.00000000 
##           region       population        fertility infant_mortality 
##       0.00000000       0.01754386       0.01773352       0.13779042 
##              gdp 
##       0.28183973
barplot(s1,col = "lightskyblue", 
        ylab = "Tỷ lệ",
        xlab = "Tên biến/cột",
        main = "Tỷ lệ missing value của các cột dữ liệu Gapminder")

2.5 Phụ lục

2.5.1 Kiến thức nâng cao liên quan đến ma trận

Các kiến thức liên quan đến ma trận trong phần này đòi hỏi bạn đọc cần có kiến thức nâng cao hơn. Nếu bạn đọc cảm thấy không cần thiết có thể bỏ qua vì các kiến thức được sử dụng trong phần này sẽ được nhắc lại khi có ứng dụng cụ thể.

2.5.1.1 Giá trị riêng và véc-tơ riêng của ma trận

2.5.1.2 Ma trận hiệp phương sai

2.5.2 Các giá trị đặc trưng của một dòng tiền tương lai

2.5.2.1 Giá trị hiện tại và tỷ suất sinh lời nội bộ

Giá trị hiện tại của một dòng tiền \(CF\) với lãi suất \(i\) tính theo kiểu gộp được tính như sau \[\begin{align} PV(CF) = \sum\limits_{t=1}^n \ \cfrac{CF_t}{(1+i)^t} \end{align}\]

Giá trị hiện tại của một dòng tiền là một thước đo cho giá trị của tài sản tạo ra dòng tiền đó. Nhìn chung, tài sản nào có giá trị hiện tại lớn hơn thì có giá trị cao hơn.

Tỷ suất sinh lời nội bộ (Internal rate of return hay IRR) là mức lãi suất \(i_0\) mà tính theo mức lãi suất này giá trị hiện tại của một dòng tiền bằng 0. Tỷ suất sinh lời nội bộ không tồn tại nếu dòng tiền tương lai của một tài sản chỉ có giá trị dương (hoặc âm).

2.5.2.2 Durations và convexity

Duration không phải là thước đo thời gian từ lúc bắt đầu đến lúc đáo hạn của dòng tiền mà là một thước đo cho sự nhạy cảm của giá trị hiện tại của dòng tiền theo sự thay đổi của lãi suất.

## 
## Attaching package: 'dplyr'
## The following object is masked from 'package:gridExtra':
## 
##     combine
## The following object is masked from 'package:kableExtra':
## 
##     group_rows
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
## 
## Attaching package: 'lubridate'
## The following objects are masked from 'package:base':
## 
##     date, intersect, setdiff, union

3 Phân tích dữ liệu bằng R

Trong phần này của cuốn sách, bạn sẽ tìm hiểu về các kỹ thuật phân tích dữ liệu, bao gồm có tiền xử lý dữ liệu, sắp xếp dữ liệu và trực quan hóa dữ liệu.

  • Tiền xử lý dữ liệu bao gồm tất cả các kỹ thuật đưa dữ liệu từ các nguồn khác nhau vào R và biến đổi thành định dạng để có thể làm việc được.

  • Sắp xếp dữ liệu bao gồm các bước biến đổi, chuyển hóa dữ liệu thành định dạnh để có thể trực quan hóa, phân tích, và xây dựng mô hình.

  • Trực quan hóa dữ liệu là một nghệ thuật biến đổi dữ liệu dưới dạng các con số, chuỗi ký tự,…, thành các biểu đồ, đồ thị hay hình ảnh sử dụng các hình dạng, màu sắc, khoảng cách để con người dễ dàng nhận thức và hiểu về dữ liệu. Trực quan hóa dữ liệu còn có thể giúp người phân tích tìm ra những giá trị ẩn chứa trong dữ liệu.

4 Nhập dữ liệu từ các nguồn khác nhau vào R

4.1 Đối tượng dùng để lưu dữ liệu trong R

Hai kiểu đối tượng thường được dùng để lưu dữ liệu trong R là \(data.frame\)\(tibble\). Chúng ta sẽ thảo luận về \(data.frame\) trước vì đây là kiểu lưu dữ liệu phổ biến. Kiểu \(tibble\) với một vài ưu điểm hơn \(data.frame\) sẽ được thảo luận trong phần tiếp theo.

4.1.1 \(data.frame\) là gì?

\(data.frame\) là đối tượng phổ biến nhất để lưu trữ dữ liệu trên cửa sổ làm việc của R. Hiểu một cách đơn giản, một \(data.frame\) là một bảng excel mà mỗi cột tương ứng với một véc-tơ và mỗi dòng tương ứng với một quan sát. Ngay khi cài đặt R, đã có nhiều đối tượng là dữ liệu kiểu \(data.frame\) đã được lưu trữ trong R và sẵn sàng sử dụng mà không cần gọi thư viện bổ sung. Để biết trên cửa sổ Rstudio đang sử dụng có những dữ liệu nào, bạn đọc sử dụng câu lệnh data()

Bạn đọc có thể thấy trên cửa sổ R Script xuất hiện một cửa sổ mới với danh sách tất cả các dữ liệu sẵn có trong R và dữ liệu sẵn có trong các thư viện được cài đặt thêm mà bạn đọc đang gọi ra trên cửa sổ làm việc. Để biết trong một thư viện đang được gọi ra trên cửa sổ Rstudio có những dữ liệu nào, bạn đọc có thể sử dụng lệnh data() kèm với tùy chọn \(package\)

library(dslabs) # gọi thư viện dslabs lên trên màn hình 
data(package = "dslabs") # cho biết có những data nào trong thư viện dslabs

Trong danh sách dữ liệu của thư viện \(dslabs\), bạn đọc có thể thấy một đối tượng có tên \(murders\). Đây là một \(data.frame\). Bạn đọc có thể kiểm tra kiểu của đối tượng này bằng hàm class()

class(murders) # trên màn hình console sẽ cho biết đây là một data frame
## [1] "data.frame"

Thông thường để có hiểu biết ban đầu về một đối tượng kiểu \(data.frame\), bạn đọc nên bắt đầu bằng đọc mô tả về dữ liệu (nếu có) bằng cách sử dụng ?

? murders # trên cửa sổ help sẽ hiển thị mô tả về murders

Nhóm các câu lệnh dưới đây giúp bạn đọc hiểu được cấu trúc của dữ liệu trong \(data.frame\) đó

View(murders) # Hiển thị data.frame dưới dạng bảng 
head(murders,k = 5) # Hiển thị k dòng đầu tiên của data.frame
##        state abb region population total
## 1    Alabama  AL  South    4779736   135
## 2     Alaska  AK   West     710231    19
## 3    Arizona  AZ   West    6392017   232
## 4   Arkansas  AR  South    2915918    93
## 5 California  CA   West   37253956  1257
## 6   Colorado  CO   West    5029196    65
str(murders) # Hiển thị cấu trúc của data.frame.
## 'data.frame':    51 obs. of  5 variables:
##  $ state     : chr  "Alabama" "Alaska" "Arizona" "Arkansas" ...
##  $ abb       : chr  "AL" "AK" "AZ" "AR" ...
##  $ region    : Factor w/ 4 levels "Northeast","South",..: 2 4 4 2 4 4 1 2 2 2 ...
##  $ population: num  4779736 710231 6392017 2915918 37253956 ...
##  $ total     : num  135 19 232 93 1257 ...
  • Hàm head() hiển thị nhanh các dòng đầu tiên của dữ liệu cho bạn đọc cái nhìn ban đầu, tuy nhiên hàm head() không hiệu quả khi dữ liệu có nhiều cột.

  • Hàm View() cho hiển thị về dữ liệu dễ nhìn nhất. Hàm View() có hạn chế khi dữ liệu có quá nhiều dòng hoặc nhiều cột và thời gian hiển thị lâu hơn so với head().

  • Hàm str() là cách hiển thị dữ liệu một cách tổng quát và hiệu quả hơn so với head() hoặc View(). Kết quả từ hàm str() với dữ liệu \(murders\) cho thấy đây là một dữ liệu dạng bảng với 5 cột (5 variables) và 51 dòng (51 observations). Ngoài ra, sử dụng hàm str() bạn đọc có thể thấy được kiểu dữ liệu của từng cột; chẳng hạn như cột \(state\) là cột chứa dữ liệu kiểu \(character\); cột \(region\) có kiểu dữ liệu là \(factor\),…

Một hàm số hiệu quả khác thường được sử dụng để bạn đọc có cái nhìn tổng quan về dữ liệu là hàm summary(). Chúng ta có thể quan sát kết quả khi sử dụng hàm `summary() với dữ liệu \(murders\) như sau

summary(murders) # in ra màn hình cột population của data.frame murders
##     state               abb                      region     population      
##  Length:51          Length:51          Northeast    : 9   Min.   :  563626  
##  Class :character   Class :character   South        :17   1st Qu.: 1696962  
##  Mode  :character   Mode  :character   North Central:12   Median : 4339367  
##                                        West         :13   Mean   : 6075769  
##                                                           3rd Qu.: 6636084  
##                                                           Max.   :37253956  
##      total       
##  Min.   :   2.0  
##  1st Qu.:  24.5  
##  Median :  97.0  
##  Mean   : 184.4  
##  3rd Qu.: 268.0  
##  Max.   :1257.0

Hàm summary() cho biết thông chi tiết hơn về giá trị trong mỗi cột.

  • Cột \(state\) và cột \(abb\) là cột mà giá trị trong đó là kiểu \(character\)

  • Cột \(region\) là kiểu factor, có thể nhận một trong bốn giá trị là \(Northeast\), \(South\), \(North\) \(Central\), hoặc \(West\) và cho biết mỗi giá trị xuất hiện bao nhiêu lần trong cột dữ liệu.

  • Các cột \(population\)\(total\) là các cột kiểu số. Chúng ta có thể thấy các giá trị lớn nhất, nhỏ nhất, giá trị trung bình và các giá trị tứ phân vị. Bạn đọc có thể hình dung ra phân phối của các giá trị trong cột giá trị kiểu số.

  • Trong trường hợp cột có giá trị không quan sát được, hàm summary() cũng sẽ cho biết có bao nhiêu giá trị này trong mỗi cột.

Để lấy ra một cột dữ liệu của một \(data.frame\) chúng ta sử dụng \(\$\). Chẳng hạn như để lấy giá trị cột \(population\) của dữ liệu \(murders\):

murders$population # in ra màn hình cột population của data.frame murders

Như đã nói ở trên, kiểu dữ liệu của cột \(region\) là kiểu \(factor\). Về bản chất, véc-tơ kiểu \(factor\) là một véc-tơ kiểu chuỗi ký tự nhưng được lưu theo một cách hiệu quả hơn, tiết kiệm bộ nhớ, và thuận lợi cho người sử dụng khi phân tích dữ liệu.

  • Dữ liệu kiểu factor sẽ lưu véc-tơ chuỗi ký tự dưới dạng vec-tơ số tự nhiên và mỗi chuỗi ký tự sẽ được cho tương ứng với một số tự nhiên. Các lưu này hiệu quả hơn về bộ nhớ khi làm việc với các véc-tơ kiểu chuỗi ký tự nếu có nhiều chuỗi ký tự bị lặp lại trong véc-tơ. Để biết một vec-tơ dạng factor có bao nhiêu giá trị riêng biệt, mỗi giá trị riêng biệt được cho tương ứng với số tự nhiên nào, và mỗi giá trị riêng biệt được lặp lại bao nhiêu lần trong véc-tơ, bạn đọc sử dụng hàm summary() hoặc hàm table()
# summary(murders$region) # Tổng hợp thông tin của vec-tơ dạng factor
table(murders$region) # cho kết quả tương tự như summary
## 
##     Northeast         South North Central          West 
##             9            17            12            13

Kết quả từ hàm table() cho thấy cột \(region\) có 4 giá trị; cách cho tương ứng mỗi chuỗi ký tự với các số lần lượt là \(Northeast \rightarrow 1\) ; \(South \rightarrow 2\); \(North Central \rightarrow 3\), và \(West \rightarrow 4\); tần suất xuất hiện của mỗi giá trị cũng được cho trong bảng: có 9 giá trị \(Northeast\), có 17 giá trị \(South\), có 12 giá trị \(North Central\), và 13 giá trị \(West\).

  • Khi lưu dữ liệu kiểu factor thay vì chuỗi ký tự nghĩa là bạn đọc đang định nghĩa dữ liệu là kiểu biến rời rạc (categorial variable). Biến có thể trực tiếp đưa vào các mô hình và không cần thực hiện thêm biến đổi nào khác.

Trong hầu hết các trường hợp bạn đọc sẽ dùng R để xử lý dữ liệu từ nguồn ngoài vào. Chúng ta sẽ sử dụng các hàm có sẵn trong R đọc dữ liệu và kết quả đầu ra của hàm này sẽ là các \(data.frame\). Trong một vài trường hợp, bạn đọc sẽ phải tự tạo \(data.frame\). Câu lệnh để tạo một \(data.frame\) (tên \(df\)) với các cột có tên lần lượt là \(id\), \(names\), \(grades\), và \(result\) được viết như sau

df<-data.frame( # hàm data.frame() dùng để tạo data.frame tên df
      id = paste("SV",1:5), # cột có tên là ID nhận giá trị "SV1",...,"SV5"
      names = c("You", "Me", "Him", "Her", "John"), # Cột names
      grades = c(5.5, 1.5, 10.0, 9.0, 7.6), # Cột grades
      result = c(TRUE, FALSE,TRUE, TRUE, TRUE)) # Cột result

Đối tượng kiểu \(data.frame\) có một vài nhược điểm khi sử dụng để lưu dữ liệu từ các nguồn khác nhau vào R. Do đó kiểu đối tượng mới được phát triển để khắc phục các nhược điểm này, đó là \(tibble\). Phần tiếp theo chúng ta sẽ thảo luận về đối tượng này.

4.1.2 \(tibble\) là một cải tiến của \(data.frame\)?

Về cơ bản một \(tibble\) là cũng có thể hiểu là một \(data.frame\) với một vài điều chỉnh để giúp việc lấy dữ liệu từ nguồn bên ngoài vào phân tích trở nên dễ dàng hơn. Ở mức độ phân tích dữ liệu thông thường, sự khác khác nhau giữa \(tibble\)\(data.frame\) là không đáng kể. Nếu cần liệt kê ra sự khác nhau cơ bản giữu hai đối tượng này thì có thể kể đến:

  • Thứ nhất: khi in một \(tibble\) ra màn hình sẽ chỉ có 10 dòng đầu được hiển thị và số lượng cột của một \(tibble\) luôn luôn khớp với kích thước cửa sổ R Console, đồng thời kiểu dữ liệu của mỗi cột sẽ được hiển thị ngay dưới tên cột
library(tibble)
trump_tweets # in một data frame ra màn hình sẽ không hiệu quả
# Hàm as_tibble đổi data.frame sang tibble
as_tibble(trump_tweets) # Hiển thị 1 tibble hiệu quả hơn.
  • Thứ hai: khi lấy dữ liệu từ bên ngoài vào trong R, \(tibble\) không đổi tên cột dù tên cột không phải là kiểu tên được phép trong R. Đồng thời, khi tạo một \(tibble\), bạn đọc có thể đặt tên cột là một kiểu tên không được phép sử dụng với tên biến thông thường.

  • Cuối cùng, khi dữ liệu từ bên ngoài được lưu vào một \(tibble\), kiểu dữ liệu sẽ không thay đổi.

Để tạo một \(tibble\), bạn đọc có thể sử dụng hàm tibble(). Bạn đọc có thể tạo ra một dữ liệu có 3 cột mà tên các cột đều không thể được sử dụng làm tên biến trong R như sau

tib<-tibble( # hàm tibble dùng để tạo tibble
  ":D" = c(1,2,3), # có thể dùng tên cột là ":D"
  ":p" = c("X","Y","Z"), # có thể dùng tên cột là ":p"
  "1" = 2 # có thể dùng tên cột là "1"
)
tib
## # A tibble: 3 × 3
##    `:D` `:p`    `1`
##   <dbl> <chr> <dbl>
## 1     1 X         2
## 2     2 Y         2
## 3     3 Z         2

Nếu thay thế đoạn lệnh trên bằng hàm data.frame() thì hàm \(data.frame\) được tạo thành sẽ tự động thay đổi tên cột

df<-data.frame( # tạo data.frame thay vì tibble
  ":D" = c(1,2,3), # data.frame sẽ đổi tên cột cho phù hợp
  ":p" = c("X","Y","Z"), # data.frame sẽ đổi tên cột cho phù hợp
  "1" = 2 # data.frame sẽ đổi tên cột cho phù hợp
)
df # hãy quan sát xem tên cột của df thay đổi như thế nào
##   X.D X.p X1
## 1   1   X  2
## 2   2   Y  2
## 3   3   Z  2

Bạn đọc có thể thấy rằng dữ liệu có tên \(df\) nếu được lưu dưới dạng \(data.frame\) thì tên các cột đã được tự động thay đổi cho thích hợp với tên biến. Những điểm khác nhau giữa \(tibble\)\(data.frame\) sẽ được tiếp tục thảo luận ở các phần tiếp theo khi chúng tôi giới thiệu về các hàm dùng để nhập dữ liệu vào R.

4.1.3 Nhập dữ liệu bằng hàm sẵn có.

Danh sách các hàm sẵn có trong R và kiểu dữ liệu tương ứng có thể nhập được liệt kê trong các bảng sau:

(#tab:unnamed-chunk-14)Danh sách hàm có sẵn để lấy dữ liệu từ nguồn bên ngoài
Hàm số Sử dụng trong trường hợp
read.table() Đọc các file có đuôi dạng .txt
read.csv() file dạng csv mà giá trị được ngăn cách bằng dấu ‘,’
read.csv2() file dạng csv mà giá trị được ngăn cách bằng dấu ‘;’
read.delim() Các file dạng text, các giá trị cách nhau bởi ký tự mà bạn đọc định nghĩa
readRDS Dữ liệu được lưu dưới dạng .rds

Khi lấy dữ liệu từ các nguồn bên ngoài vào bằng các câu lệnh có sẵn, tên của các cột dữ liệu có thể bị thay đổi do một số tên cột không thể được dùng để đặt tên của \(data.frame\). Do đó, bạn đọc hãy luôn kiểm tra lại tên các cột dữ liệu sau khi đọc. Hàm names() cho biết tên các cột của một \(data.frame\).

df<-read.csv(header = TRUE, 
              text = "@1,@2 
                      1,2 
                      3,4") # sử dụng read.csv để đọc đoạn text
names(df) # hiển thị tên của các cột
## [1] "X.1" "X.2"

Để đổi tên của \(data.frame\) ở trên, bạn đọc cần gán \(names(df)\) bằng một véc-tơ chứa tên các cột. Hãy đảm bảo rằng độ dài của vec-tơ chứa tên cột bằng số cột của \(data.frame\)

names(df)<-c("@1","@2") # đổi tên 2 cột của data.frame df
df # in data.frame df
##   @1 @2
## 1  1  2
## 2  3  4

4.1.4 Nhập dữ liệu bằng thư viện \(readr\).

Các câu lệnh để đọc dữ liệu của thư viện \(readr\) tương tự như các câu lệnh sẵn có, nhưng đặc biệt hiệu quả hơn về thời gian đọc dữ liệu. Hàm số dùng để đọc các file định dạng \(csv\) trong thư viện \(readr\) là hàm read_csv(). Để so sánh thời gian đọc dữ liệu vào R của hàm read_csv() và hàm read.csv() chúng ta sẽ tạo hai file dữ liệu bao gồm test1.csv và test2.csv

x<-matrix(rnorm(10^6),10^2,10^4) # tạo thành 1 ma trận 100 hàng, 10^4 cột
write.csv(x,"test1.csv") # lưu ma tran thanh file .csv
x<-matrix(rnorm(10^7),10^2,10^5) # tạo thành 1 ma trận 100 hàng, 10^5 cột
write.csv(x,"test2.csv") # lưu ma tran thanh file .csv

Bạn đọc có thể kiểm tra kích thước của các file test1.csv và test2.csv lần lượt là khoảng 18 Mega byte và 180 Mega byte. Chúng ta sẽ kiểm tra thời gian mà các hàm read.csv()read_csv() nhập dữ liệu đối với dữ liệu test1.csv trước:

start<-proc.time() # lưu lại thời điểm trước khi chạy read.csv
dat<-read.csv("test1.csv") # dùng hàm read.csv để load dữ liệu 
proc.time() - start # tính thời gian hàm read.csv chạy

start<-proc.time() # lưu lại thời điểm trước khi chạy read_csv
dat<-read_csv("test1.csv") # dùng hàm read_csv để load dữ liệu 
proc.time() - start # tính thời gian hàm read_csv chạy

Đối với dữ liệu \(test1.csv\) thì thời gian nhập dữ liệu của read_csv() có nhanh hơn nhưng không có sự khác biệt đáng kể. Tuy nhiên sự khác biệt sẽ rõ ràng khi nhập dữ liệu \(test2.csv\). Bạn đọc cân nhắc khi dùng hàm read.csv() đọc dữ liệu thời gian nhập dữ liệu có thể lên đến hơn 20 phút.

start<-proc.time() # lưu lại thời điểm trước khi chạy read.csv
dat<-read.csv("test2.csv") # !!! THỜI GIAN CHẠY CÓ THỂ LÊN ĐẾN 20-25 phút
proc.time() - start # tính thời gian hàm read.csv chạy

start<-proc.time() # lưu lại thời điểm trước khi chạy read_csv
dat<-read_csv("test2.csv") # dùng hàm read_csv để load dữ liệu 
proc.time() - start # tính thời gian hàm read_csv chạy

Hàm read_csv() sẽ mất khoảng 2 phút để đọc dữ liệu \(test2.csv\), nghĩa là thời gian tiết kiêm lên đến hơn 10 lần! Danh sách các hàm để đọc dữ liệu trong gói lệnh \(readr\) như sau

(#tab:unnamed-chunk-19)Danh sách hàm đọc dữ liệu của readr
Hàm số Sử dụng trong trường hợp
read_csv() file dạng csv mà giá trị được ngăn cách bằng dấu ‘,’
read_csv2() file dạng csv mà giá trị được ngăn cách bằng dấu ‘;’
read_tsv() Các file dạng text mà các giá trị cách nhau bởi khoảng trống
read_delim() Các file dạng text mà các giá trị cách nhau bởi ký tự bất kỳ

Một sự khác biệt cơ bản khác của các hàm đọc dữ liệu trong readr đó là dữ liệu được lưu vào một Tibble thay vì một Data frame. Điều này giúp cho dữ liệu không bị thay đổi định dạng và giữ nguyên tên cột. Các lưu ý khác khi bạn đọc sử dụng các hàm số đọc dữ liệu của readr

  • Các hàm số trong readr luôn hiểu hàng đầu tiên của dữ liệu là tên của mỗi cột. Do đó, bạn đọc cần sử dụng tham số \(col_names = FALSE\) nếu không muốn readr hiểu hàng đầu tiên là tên của mỗi cột dữ liệu.
library(readr)
# Kết quả sẽ là một Tibble 1 hàng và 3 cột
read_csv("1,2,3 
         4,5,6") # tên các cột là "1", "2", và "3"
## # A tibble: 1 × 3
##     `1`   `2`   `3`
##   <dbl> <dbl> <dbl>
## 1     4     5     6
# Kết quả sẽ là một Tibble 2 hàng và 3 cột
read_csv("1,2,3 
         4,5,6", col_names = FALSE) # readr tự động đặt tên các cột X1, X2, X3
## # A tibble: 2 × 3
##      X1    X2    X3
##   <dbl> <dbl> <dbl>
## 1     1     2     3
## 2     4     5     6
  • Trong nhiều file dữ liệu các hàng đầu tiên là các mô tả về dữ liệu nên khi sử dụng readr, bạn đọc có thể sử dụng tùy biến \(skip = k\) để loại bỏ \(k\) dòng đầu tiên trong file dữ liệu.
# Kết quả sẽ là một Tibble 2 hàng và 3 cột
read_csv("Trường ĐHKTQD
        Khoa toán Kinh tế
         1,2,3 
         4,5,6", col_names = FALSE, skip = 2) # readr sẽ không đọc 2 dòng đầu
## Rows: 2 Columns: 3
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## dbl (3): X1, X2, X3
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
## # A tibble: 2 × 3
##      X1    X2    X3
##   <dbl> <dbl> <dbl>
## 1     1     2     3
## 2     4     5     6

Bạn đọc cũng có thể sử dụng tham số \(col_names\) để gán giá trị cho tên các cột, tuy nhiên lời khuyên của chúng tôi là bạn đọc hãy đặt tên cho các cột bằng hàm \(names()\) sau khi lưu dữ liệu vào tibble để tránh sự phức tạp không đáng có.

Cách sử dụng các hàm khác ngoài read_csv() bạn đọc có thể tham khảo trong hướng dẫn của gói lệnh \(readr\). Sau khi đọc qua hướng dẫn, bạn đọc hãy thử kiểm tra xem các câu lệnh sau có vấn đề gì và nếu có thể, bạn đọc hãy thử lựa chọn hàm hoặc thêm tham số phù hợp để đọc dữ liệu

read_csv("x,y\n1,2,3\n4,5,6") # \n thay cho xuống dòng
read_csv("x,y,z\n1,2\n1,2,3,4") 
read_csv("x,y\n\",1,\n,a,b",col_names = FALSE) 
read_csv("x;y\n1;2\nx;y") # Thử hàm số khác
read_csv("x|y\n1|2") # Thử hàm số khác

4.1.5 Tương tác giữa R và Microsoft Excel

4.1.5.1 Đọc dữ liệu lưu dưới định dạng của Excel

Microsoft Excel là rất phổ biến trong môi trường làm việc công sở. Do đó, dữ liệu nhận sẽ có thể là các file định dạng (.xls, .xlsx, .xlsb, .xlsm … ). Các gói lệnh \(openxlsx\)\(readxl\) thường được sử dụng để đọc dữ liệu từ các file có định dạng như vậy.

4.1.5.2 Tương tác với Microsoft Excel bằng gói lệnh \(openxlsx\)

Ngoài việc lấy dữ liệu từ các file được lưu dưới định dạnh của Excel, bạn đọc cũng có thể sử dụng gói lệnh \(openxlsx\) để làm việc với các excel workbook thay vì làm việc trực tiếp trên workbook.

4.1.6 Lấy dữ liệu từ một hệ cơ sở dữ liệu

Có thể sử dụng R như một công cụ để kết nối và thực hiện các câu lệnh truy vấn vào các cơ sở dữ liệu. Để làm được điều này, trước tiên, bạn đọc cần phải cài đặt một Open Database Connectivity (ODBC), được gọi là kết nối cơ sở dữ liệu mở. Kết nối này giúp cho hệ điều hành máy tính tương thích với hệ quản lý cơ sở dữ liệu mà bạn sử dụng. Nhóm tác giả sử dụng hệ điều hành Windows và hệ quản trị cơ sở dữ liệu MySQL nên chúng tôi sẽ lựa chọn ODBC phù hợp. Bạn đọc tham khảo tại địa chỉ https://dev.mysql.com/downloads/connector/odbc/. Tại thời điểm nhóm tác giả viết cuốn sách này ODBC cho hệ điều hành Windows đang là phiên bản 8.0

Sau khi cài đặt ODBC lên hệ điều hành, bạn đọc đã có thể sử dụng R để truy cập vào một cơ sở dữ liệu và thực hiện các câu lệnh truy vấn dữ liệu trên cơ sở dữ liệu đó trên R với sự trợ giúp của thư viện \(DBI\). Sau khi cài đặt thư viện \(DBI\), bạn đọc cần tạo một kết nối giữa R và cơ sở dữ liệu bằng hàm \(dbConnect\).

library(DBI)
con <- dbConnect(odbc::odbc(), .connection_string = "Driver={MySQL ODBC 8.0 Unicode Driver};", 
    Server = "ten_serve", Database = "db_name", UID = "ID", PWD = "password")

Trong đó “ten_serve” là địa chỉ local hoặc server lưu trữ cơ sở dữ liệu, “db_name” là tên của cơ sở dữ liệu, ID và password lần lượt là tên và password mà bạn đọc tự tạo hoặc được cấp để truy cập vào cơ sở dữ liệu. Sau khi đã tạo được kết nối, bạn đọc có thể thực hiện bất kỳ câu lệnh truy vấn dữ liệu nào từ R với hàm “DBI::dbGetQuery()”. Chẳng hạn, bạn đọc muốn lấy ra thông tin của tất cả những người có ngày sinh là ngày 01 tháng 01 năm 2000 từ một bảng có tên là Life_Insured từ một cơ sở dữ liệu tên là \(tktdb\), bạn đọc thực hiện như sau

sql<-"select * from tktdb.Life_Insured 
      where DOB = '2000-01-01'" # Viết đúng câu lệnh truy vấn từ MySQL
df<-DBI::dbGetQuery(sql) # data.frame df sẽ lưu kết quả của câu lệnh truy vấn

TIP: Khi làm việc với dữ liệu được trích xuất từ một hệ cơ sở dữ liệu, bạn đọc hãy cố gắng thực hiện các phép biến đổi, sắp xếp dữ liệu bằng SQL thay vì thực hiện biến đổi trên R vì các hệ quản trị cơ sở dữ liệu thực hiện các chức năng này nhanh hơn R rất nhiều.

4.2 Bài tập

4.3 Phụ lục

5 Tiền xử lý dữ liệu

Tiền xử lý dữ liệu là một công việc đòi hỏi sự tỉ mỉ, cẩn thận và cũng là một trong những bước quan trọng nhất trong một quy trình làm việc với dữ liệu. Tiền xử lý dữ liệu là tập hợp tất cả các bước kỹ thuật nhằm đảm bảo cho dữ liệu bạn sử dụng để phân tích được đảm bảo về định dạng, giá trị và ý nghĩa. Hiểu một cách đơn giản, tiền xử lý dữ liệu là biến dữ liệu thô thành dữ liệu có thể sử dụng được để phân tích và đưa ra kết quả.

Khi làm việc với dữ liệu, thực tế là đến hơn 50% các trường hợp bạn đọc sẽ nhận được những dự liệu dạng thô chưa qua xử lý. Nếu những dữ liệu này được nhập và xuất ra qua một hệ thống được phát triển đầy đủ, tiền xử lý dữ liệu chỉ cần qua một vài bước cơ bản để đi đến kết quả. Trong trường hợp dữ liệu bạn nhận được là dữ liệu được nhập một cách thủ công, thông qua nhiều người nhập thì đây thực sự sẽ là một vấn đề lớn. Tiền xử lý dữ liệu trong hoàn cảnh như vậy có thể chiếm 80% - 90% thời gian công việc của bạn!.

5.1 Tiền xử lý dữ liệu là gì?

Các vấn đề thường gặp phải khi làm việc với một dữ liệu từ các nguồn khác nhau thường xuất phát từ hai vấn đề

  • Dữ liệu sai định dạng: trong cùng một cột dữ liệu có các biến kiểu khác nhau hoặc kiểu của biến không đúng như quy ước.

  • Dữ liệu chứa giá trị không quan sát được hoặc chứa các giá trị ngoại lai (outliers).

Ví dụ như bạn đọc nhận được dữ liệu về 3 ứng cử viên từ bộ phận nhân sự như sau với yêu cầu về cho biết độ tuổi trung bình của những ứng cử viên và tỷ lệ Nam/Nữ ứng tuyển

(#tab:unnamed-chunk-27)Dữ liệu thô từ nguồn bên ngoài
Họ và tên Ngày sinh Giới tính
Nguyễn Văn An 01/02/98 Nam
Trần Văn Cường 12/17/1999 NA
Lê Thị Loan 1-1-1992 NA

Đây là một dữ liệu không thể sử dụng để phân tích được bởi vì giá trị trong các cột ngày sinh là không đúng định dạng ngày tháng đồng thời có các giá trị không quan sát được ở cột giới tính. Nếu sử dụng dữ liệu này để phân tích mà bỏ qua việc tiền xử lý dữ liệu thì kết quả sẽ sai lệch hoàn toàn so với bản chất của dữ liệu:

  • Tính độ tuổi trung bình của các ứng cử viên là không thể thực hiện được với dữ liệu này bởi vì cột ngày sinh đang là dạng chuỗi ký tự và định dạng ngày tháng là rất lộn xộn

  • Nếu bỏ qua những giá trị không có quan sát, tỷ lệ giới tính Nam là 100%. Liệu con số này có thực sự đúng?

Tiền xử lý dữ liệu không chỉ bao gồm các công cụ kỹ thuật mà còn yêu cầu cả kiến thức phổ thông và kiến thức nghiệp vụ của người làm dữ liệu. Khi có vấn đề gây khó hiểu về dữ liệu nhận được, điều hết cần làm đó là liên hệ với người chủ dữ liệu để kiểm tra lại thông tin. Khi việc này là không thể thực hiện được, người xử lý dữ liệu sẽ phải đưa ra các phán đoán về dữ liệu đó dựa trên hiểu biết của mình.

Với cột ngày sinh của các nhân viên:

  • Giá trị “01/02/98” có khả năng cao là ngày 01 tháng 02 năm 1998 do quy ước phổ biến ở Việt Nam là viết theo thứ tự ngày -> tháng -> năm.

  • Giá trị “12/17/1999” có khả năng cao là ngày 17 tháng 12 năm 1999. Khi gặp các trường hợp này nhiều khả năng người nhập dữ liệu sử dụng format ngày tháng của Microsoft Excel.

  • Giá trị “1-1-1992” có khả năng cao là ngày 01 tháng 01 năm 1992.

Như vậy với mỗi giá trị trong cột ngày sinh, bạn đọc cần một phép biến đổi khác nhau

DOB<-rep(as.Date("1900-01-01"),3) # tạo vector dạng date độ dài 3
DOB[1]<-as.Date("01/02/98", format = "%d/%m/%y")
DOB[2]<-as.Date("12/17/1999", format = "%m/%d/%Y")
DOB[3]<-as.Date("1-1-1992", format = "%d-%m-%Y")

Vec-tơ \(DOB\) chứa giá trị ngày sinh dạng ngày tháng của các ứng cử viên và bạn đọc đã có thể sử dụng các hàm số có sẵn để tính tuổi của các ứng cử viên.

Với cột giới tính của nhân viên:

  • Giới tính của ứng cử viên Trần Văn Cường là không quan sát được tuy nhiên theo tên của ứng cử viên thì nhiều khả năng đây là Nam.

  • Giới tính của ứng cử viên Lê Thị Loan là không quan sát được tuy nhiên theo tên của ứng cử viên thì nhiều khả năng đây là Nữ.

Sau những bước xử lý như trên, chúng ta đã có một dữ liệu được định dạng chính xác và đã có thể đưa ra các phân tích về dữ liệu

(#tab:unnamed-chunk-30)Dữ liệu sau tiền xử lý
Họ và tên Ngày sinh Giới tính
Nguyễn Văn An 1998-02-01 Nam
Trần Văn Cường 1999-12-17 Nam
Lê Thị Loan 1992-01-01 Nữ

Đây là một ví dụ điển hình của tiền xử lý dữ liệu. Dữ liệu bạn đọc nhận được sẽ hiếm khi được định dạng chuẩn và sẵn sàng để phân tích giống như những dữ liệu sẵn có trên R. Để xử lý những giá trị sai định dạng, và điền vào dữ liệu các giá trị không quan sát, loại bỏ các giá trị ngoại lai, …, người làm dữ liệu phải sử dụng kiến thức phổ thông và kiến thức nghiệp vụ để dưa ra dự đoán tốt nhất có thể.

5.2 Định dạng lại các cột dữ liệu sử dụng thư viện \(readr\)

5.2.1 Quy tắc định đạng cột của \(readr\)

Mỗi thư viện đọc dữ liệu sẽ có các quy tắc khác nhau khi đọc và định dạng lại dữ liệu khi lưu trên R. Chúng ta sẽ tập trung vào cách thư viện “readr” đọc dữ liệu. Khi đọc một file vào R, các hàm đọc dữ liệu của thư viện \(readr\) cố gắng dự đoán kiểu dữ liệu của từng cột bằng cách sử dụng 1000 hàng dữ liệu đầu tiên dựa trên nguyên tắc như sau:

Giá trị trong cột dữ liệu \(readr\) dự đoán
Cột dữ liệu chỉ bao gồm TRUE, FALSE, True, False, true, false, F, T, t, f Kiểu logic
Cột dữ liệu chỉ bao gồm số, số thập phân sử dụng dấu ‘.’ , Kiểu số
Lê Thị Loan 1-1-1992

Lưu ý rằng các giá trị không quan sát được không ảnh hưởng đến việc \(readr\) dự đoán kiểu dữ liệu của một cột. Khi đọc dữ liệu kiểu số, việc sử dụng dấu thập phân là ‘.’ (dữ liệu từ các nước sử dụng ngôn ngữ tiếng Anh) hay ‘,’ (dữ liệu từ Việt Nam, Pháp) có thể làm cho giá trị của biến kiểu số thay đổi về bản chất. Bạn đọc không sử dụng tham số nào khác khi sử dụng \(readr\), số thập phân luôn được hiểu là ‘.’ khi bạn đọc sử dụng read_csv() và số thập phân sẽ là ‘,’ nếu bạn đọc dùng read_csv2().

file<-"C1;C2;C3;C4; C5
         1e-10;2.2;1.0;TRUE; 1.0.0.0
         Inf;3,2;1,000.0;1;10%" 
read_csv2(file)
## # A tibble: 2 × 5
##        C1    C2    C3 C4    C5     
##     <dbl> <dbl> <dbl> <chr> <chr>  
## 1   1e-10  22      10 TRUE  1.0.0.0
## 2 Inf       3.2     1 1     10%

Từ kết quả trên có thể thấy rằng \(readr\) sẽ bỏ qua dấu ‘,’ trong các số khi bạn đọc dùng read_csv() và bỏ qua ‘.’ với biến kiểu số khi bạn đọc dùng read_csv2(). Đối với biến kiểu logic, bạn đọc có thể kiểm tra kiểu dữ liệu của các cột dưới đây đều là kiểu logic sau khi dữ liệu được đọc bằng read_csv()

file<-"X1,X2,X3,X4,X5,X6
        TRUE,t,True,false,F,
        F,F,FALSE,T,f,True"
read_csv(file)
## # A tibble: 2 × 6
##   X1    X2    X3    X4    X5    X6   
##   <lgl> <lgl> <lgl> <lgl> <lgl> <lgl>
## 1 TRUE  TRUE  TRUE  FALSE FALSE NA   
## 2 FALSE FALSE FALSE TRUE  FALSE TRUE

Để hiểu kỹ hơn cách \(readr\) dự đoán kiểu giá trị trong cột, bạn đọc có thể đọc hướng dẫn của hàm guess_parse().

5.2.2 Định dạng cột bằng các hàm parse_*()

Với các cột mà không thể xác định được kiểu dữ liệu, thư viện \(readr\) sẽ lưu dưới dạng vec-tơ kiểu chuỗi ký tự. Để làm việc được trên dữ liệu, bạn đọc cần định dạng lại các cột cho đúng với mong muốn. Các hàm thuộc nhóm parse_*() trong thư viện \(readr\) hỗ trợ bạn đọc làm việc này. Nhóm hàm parse_*() có đầu vào là một véc-tơ kiểu chuỗi ký tự và đầu ra sẽ là kiểu dữ liệu mà bạn đọc mong muốn. Đối với mỗi kiểu dữ liệu, hàm parse_*() tương ứng sẽ có các tùy biến phù hợp. Trong phần tiếp theo của cuốn sách chúng tôi sẽ lần lượt giới thiệu các nhóm hàm parse_*() tương ứng với biến kiểu logic, biến kiểu số, biến kiểu thời gian và biến kiểu chuỗi ký tự.

5.2.2.1 Định dạng véc-tơ kiểu logic.

Định dạng lại một véc-tơ kiểu chuỗi ký tự thành kiểu logic là đơn giản nhất. Hàm số sử dụng trong trường hợp này là parse_logical(). Bạn đọc hãy quan sát ví dụ sau:

x<-c("TRUE","True","1","0","2",".","@","FALSE","false","f","F","T","t","true","false")
parse_logical(x, na = c(".", "@"))
##  [1]  TRUE  TRUE  TRUE FALSE    NA    NA    NA FALSE FALSE FALSE FALSE  TRUE
## [13]  TRUE  TRUE FALSE
## attr(,"problems")
## # A tibble: 1 × 4
##     row   col expected           actual
##   <int> <int> <chr>              <chr> 
## 1     5    NA 1/0/T/F/TRUE/FALSE 2

Bạn đọc có thể thấy rằng tất cả các giá trị nằm trong véc-tơ \(x\) ở trên, ngoại trừ hai ký tự đặc biệt đã được khai báo trong hàm parse_logical()\("."\)\("@"\), thì chỉ có số 2 là không thể đổi sang biến kiểu logic. parse_logical() tự động đổi ký tự \("1"\) thành \(TRUE\) và ký tự \("0"\) thành \(FALSE\). Trong trường hợp véc-tơ \(x\) có kích thước lớn, các phần tử không thể đổi sang kiểu logic sẽ được lưu vào một \(tibble\). Bạn đọc sử dụng hàm problems() để lấy các các giá trị này.

x1<-sample(x, 10^4, replace = TRUE)
y<-parse_logical(x1)
problems(y)
## # A tibble: 1,915 × 4
##      row   col expected           actual
##    <int> <int> <chr>              <chr> 
##  1     3    NA 1/0/T/F/TRUE/FALSE 2     
##  2    13    NA 1/0/T/F/TRUE/FALSE 2     
##  3    21    NA 1/0/T/F/TRUE/FALSE .     
##  4    28    NA 1/0/T/F/TRUE/FALSE @     
##  5    32    NA 1/0/T/F/TRUE/FALSE @     
##  6    33    NA 1/0/T/F/TRUE/FALSE 2     
##  7    34    NA 1/0/T/F/TRUE/FALSE .     
##  8    39    NA 1/0/T/F/TRUE/FALSE .     
##  9    47    NA 1/0/T/F/TRUE/FALSE 2     
## 10    55    NA 1/0/T/F/TRUE/FALSE .     
## # … with 1,905 more rows

Cột \(row\) cho biết vị trí của các phần tử trong véc-tơ \(x1\) không thể đổi sang kiểu logic. Giá trị của các phần tử này nằm trong cột \(actual\). Bạn đọc có thể quan sát các giá trị trong cột \(actual\) để tìm hiểu nguyên nhân tại sao parse_logical() không thể hoạt động trên các giá trị này.

5.2.2.2 Định dạng véc-tơ kiểu số.

Khi \(readr\) không thể tự định dạng một véc-tơ có kiểu số, các vấn đề thường gặp phải là:

  • Cách đánh số thập phân của các số trong véc-tơ. Tại Việt Nam số thập phân được sử dụng là dấu “,” trong khi R hiểu số thập phân là dấu “.”. Nhiều quốc gia khác trên thế giới cũng sử dụng dấu thập phân là dấu “,”.

  • Cách viết các số sử dụng cùng với các ký tự “.” hoặc “,” để người đọc dễ dàng đọc số đó. Chẳng hạn như tại Việt Nam, chúng ta viết số 1 tỷ như sau: 1.000.000.000; tại Thụy Sỹ số 1 tỷ được viết thành 1’000’000’000; chúng ta cần định dạng lại cho các giá trị kiểu như vậy để R hiểu được đây là các con số.

  • Khi các con số đi kèm theo đơn vị, chẳng hạn như đi kèm với ký hiệu tiền tệ: “100.000 đồng”, “100.000 vnd”, hoặc đi kèm với ký hiệu % như “50%”, chúng ta \(readr\) cũng sẽ không thể tự động chuyển đổi sang kiểu số.

Bạn đọc có thể sử dụng parse_double() hoặc parse_number() khi gặp phải các vấn đề ở trên. Chẳng hạn như khi gặp vấn đề về dấu “,” đối với dấu thập phân (với các số thập phân viết theo kiểu Việt Nam), bạn đọc sử dụng parse_number() với tùy biến locale = locale(decimal_mark = ",") để định dạng cho véc-tơ kiểu ký tự:

x<-c("0,5","1,5") # véc-tơ chứa các số 0,5 và 1,5; dấu thập phân là dấu ","
parse_number(x, locale = locale(decimal_mark = ","))
## [1] 0.5 1.5

Khi gặp phải vấn đề về thứ hai, chúng ta sử dụng tùy biến \(grouping\_mark\) trong hàm \(locate()\). Trong ví dụ dưới đây sử dụng đồng thời hai tùy biến \(decimal\_mark\)\(grouping\_mark\) của hàm locate()

x<-c("1.000,5","1.000.000,5") # véc-tơ chứa các số 1000,5 và 1000000,5; dấu thập phân là dấu ","
parse_number(x, locale = locale(decimal_mark = ",",
                                grouping_mark = "."))
## [1]    1000.5 1000000.5

Khi gặp phải chuỗi ký tự chứa biến kiểu số đi kèm với đơn vị tiền tệ, hoặc dấu “%”, hàm \(parse_number()\) vẫn rất hiệu quả trong việc đổi chuỗi ký tự về kiểu số

x<-c("1.000,5 đồng","1.000.000,5 vnd")
parse_number(x, locale = locale(decimal_mark = ",",
                                grouping_mark = "."))
## [1]    1000.5 1000000.5

5.2.2.3 Định dạng véc-tơ kiểu thời gian

Hàm số parse_datetime() có thể sử dụng để chuyển đổi các véc-tơ kiểu chuỗi ký tự sang véc-tơ kiểu thời gian và véc-tơ kiểu ngày tháng.

x<-c("1/2/2023", "23/10/2023 ", "01/01/1900")
parse_datetime(x, format = "%d/%m/%Y",
               na = c("01/01/1900"))
## [1] "2023-02-01 UTC" "2023-10-23 UTC" NA

Hai tùy biến của hàm parse_datetime() mà bạn đọc cần lưu ý là \(na\)\(format\). Tùy biến \(na\) như đã sử dụng ở phần trước là một véc-tơ chứa các giá trị mà bạn đọc cho rằng đây là các giá trị không quan sát được. Trong véc-tơ \(x\) ở trên, giá trị “01#01#1990” nếu không có trong tùy biến \(na\) sẽ có kết quả là một ngày tháng có ý nghĩa trong véc-tơ kết quả. Tuy nhiên, bằng một cách nào đó, nếu bạn đọc biết rằng giá trị này do người nhập liệu đưa vào do không quan sát được biến đó, việc chuyển đổi biến thành giá trị ngày tháng sẽ làm sai lệch phân tích. Do đó bạn đọc cần khai báo giá trị này vào trong véc-tơ \(na\).

Tùy biến \(format\) trong hàm parse_datetime() là để bạn đọc gợi ý cho R định dạng của biến kiểu ngày tháng. Khi gán giá trị cho \(format\) bạn đọc cần lưu ý

  • Mỗi thành phần của biến thời gian (ngày, tháng, năm, giờ, phút, giây,…) được định nghĩa bắt đầu bằng \("%"\) và theo sau 1 chữ cái, chẳng hạn như bạn đọc sử dụng “%Y” khi muốn nói với R rằng biến kiểu thời gian nằm trong chuỗi ký tự được sử dụng 4 chữ số để chỉ định.

  • Các ký tự không liên quan đến các thành phần của thời gian, ngoại trừ các khoảng trắng phía trước và sau biến thời gian, cần phải được khai báo chính xác.

x<-c(" 1@2@2023-23#25#01  ", "  23@10@2023-01#06#59 ", "01@01@2023-00:00:00")
parse_datetime(x, format = "%d@%m@%Y-%H#%M#%S")
## [1] "2023-02-01 23:25:01 UTC" "2023-10-23 01:06:59 UTC"
## [3] NA

Từ ví dụ trên bạn đọc có thể thấy rằng

  • Cần khai báo chính xác các ký tự nằm giữa các biến thời gian. Ký tự \("@"\) nằm giữa các giá trị ngày, tháng, năm; phân tách giữa ngày tháng với thời gian trong ngày là ký tự \("-"\); phân tách giữa các thành phần của thời gian trong ngày là ký tự \("#"\). Tất cả đều cần phải được khai báo chính xác với tùy biến \(format\). Giá trị thứ ba trong véc-tơ \(x\) gặp vấn đề vì phân tách giữa các thành phần của thời gian trong ngày sử dụng dấu “:”

  • Các khoảng trắng nằm trước và sau các chuỗi ký tự được bỏ qua và không ảnh hưởng đến kết quả.

Để biết chính xác cách gán giá trị cho tùy biến \(format\), bạn đọc nên tham khảo hướng dẫn sử dụng hàm parse_datetime(). Chúng tôi tóm tắt cách định dạng các thành phần của một biến thời gian trong bảng dưới đây:

Thành phần Định dạng chi tiết
Năm %Y (4 chữ số) và %y (1 đến 2 chữ số)
Tháng %m (1-2 chữ số), %b (tên tháng viết tắt), %B (tên tháng đầy đủ)
Ngày %d (1-2 chữ số)
Giờ %H (1-2 chữ số)
Phút %M (1-2 chữ số)
Giây %S (1-2 chữ số)

Lưu ý rằng khi bạn đọc sử dụng %y, các ký tự “00” đến “69” sẽ được chuyển thành năm 2000 đến năm 2069 trong khi các ký tự từ “70” đến 99 sẽ được chuyển thành năm 1970 đến 1999. Ngoài ra, thành phần tháng của biến thời gian trong nhiều dữ liệu thường được viết dưới dạng chuỗi ký tự thay vì sử dụng số. Do đó bạn đọc cần các gợi ý %b hoặc %B để R có thể hiểu được:

x<-c("sep 21, 23 ", "  JAN 1, 69 ", "Dec 25, 70")
parse_datetime(x, format = "%b %d, %y")
## [1] "2023-09-21 UTC" "1969-01-01 UTC" "1970-12-25 UTC"

5.2.3 Định dạng cột kiểu chuỗi ký tự

Khi bạn đọc dùng \(readr\) để đọc dữ liệu từ nguồn ngoài vào R, cột dữ liệu không rõ định dạng sẽ được lưu dưới dạng véc-tơ chuỗi ký tự. Vậy tại sao cần định dạng lại thành véc-tơ kiểu chuỗi ký tự? Nghe có vẻ vô lý nhưng đây lại là vấn đề phức tạp nhất trong định dạng lại cột dữ liệu. Để hiểu vấn đề này bạn đọc cần tìm hiểu một chút về cách máy tính điện tử lưu và mở một chuỗi ký tự. Giả sử bạn đọc muốn gửi một dữ liệu chứa ký tự “a” đến một máy tính khác. Sau khi viết ký tự “a” lên một phần mềm soạn thảo văn bản nào, bạn đọc sẽ cần lưu ký tự “a” lên máy tính của bạn. Tất nhiên máy tính của bạn sẽ không thể ghi nhớ chữ “a” một cách tượng hình mà sẽ mã hóa (hay thuật ngữ chuyên ngành gọi là \(encode\)) chữ “a” thành một đoạn mã nhị phân bao gồm 0 và 1 mà máy tính có thể lưu được. Khi bạn gửi dữ liệu sang một máy tính khác, đoạn mã bao gồm 0 và 1 đó sẽ được gửi đi. Khi máy tính điện tử khác mở dữ liệu, đoạn mã nhị phân sẽ được giải mã (thuật ngữ chuyên ngành gọi là \(decode\)) để hiển thị. Sẽ không có vấn đề gì xảy ra nếu quy tắc mã hóa và giải mã được thống nhất và chữ “a” sẽ được hiển thị chính xác trên máy tính thứ hai.

Thực tế là trước khi có bộ mã hóa và quy tắc mã hóa chung được công nhận rộng rãi như Unicode và UTF-8, rất khó để có sự thống nhất quy tắc mã hóa ký tự. May mắn là đến thời điểm chúng tôi viết cuốn sách này đa số các hệ điều hành, hệ soạn thảo văn bản,… đều sử dụng bảng mã Unicode và bộ mã hóa UTF-8. Giải thích chi tiết về bộ mã hóa hay quy tắc mã hóa là rất phức tạp và vượt quá nội dung của cuốn sách này. Chúng tôi chỉ cần bạn đọc hiểu về Unicode và UTF-8 như sau:

  • Unicode là một bảng mã chuẩn được công nhận rộng rãi cho biết quy tắc cho tương ứng hầu hết các ký tự từ đơn giản đến phức tạp, kể cả các ngôn ngữ sử dụng ký tự tượng hình phức tạp như chữ Hán của tiếng Trung Quốc, tiếng Nhật, chữ Nôm của tiếng Việt, với một số nằm giữa số 0 đến số \(10FFFF\) khi viết theo hệ 16. Một số khi viết trong hệ 16 có thể sử dụng (0, 1, …, 9, A, B, C, D, E, F) để biểu diễn, do đó số các ký tự mà bảng mã Unicode có thể đưa vào là \(16^4 + 16^5 = 1.114.112\) ký tự, bao gồm \(16^5\) số từ 0 đến FFFFF và \(16^4\) số từ 100000 đến 10FFFF.

  • UTF-8 là quy tắc lưu các số viết trong hệ 16 của bảng mã Unicode thành các chuỗi nhị phân 0 và 1 mà máy tính có thể phân biệt được. Số 8 ở đây có nghĩa là 8 bit hay một byte là 8 giá trị 0 và 1 đứng liền nhau. Một ký tự bất kỳ trong bảng mã Unicode đều có thể được mã hóa thành 1, 2, 3 hoặc nhiều byte theo quy tắc mã hóa UTF-8.

Quay trở lại vấn đề định dạng lại dữ liệu kiểu chuỗi ký tự, sẽ không có vấn đề xảy ra nếu người nhập liệu sử dụng bộ mã hóa UTF-8 bởi \(readr\) luôn sử dụng UTF-8 để giải mã. Trong thực tế thì vẫn còn một số hệ thống, hoặc hệ soạn thảo văn bản sử dụng cách mã hóa không tương thích với UTF-8. Giả sử khi đọc một dữ liệu từ nguồn ngoài vào bằng read_csv() và cho kết quả như sau

x<-read_csv("../KHDL_KTKD/Dataset/Book1.csv")
x
## # A tibble: 5 × 2
##   A              B         
##   <chr>          <chr>     
## 1 "l\xea"        20.000 vnd
## 2 "t\xe1o"       35.000 vnd
## 3 "qu\xfdt"      30.000 vnd
## 4 "c\xe0 t\xedm" 5.500 vnd 
## 5 "m\xedt"       10.000 vnd

Cột \(A\) của dữ liệu đã không được lưu bằng mã hóa UTF-8 nên thư viện \(readr\) không hiển thị được các giá trị có ý nghĩa. Để định dạng lại cột dữ liệu, bạn đọc sử dụng hàm parse_character() với tùy biến \(encoding\). Không dễ để biết được dữ liệu đã được mã hóa bằng bộ mã hóa nào. Thư viện \(readr\) cung cấp hàm guess_encoding() hỗ trợ bạn đọc dự đoán một biến kiểu chuỗi ký tự đã được mã hóa bẳng bộ mã hóa nào. Tuy nhiên trải nghiệm của chúng tôi với hàm số này là không tốt. Lời khuyên của chúng tôi là bạn đọc nếu có thể hãy tìm hiểu nguồn gốc của dữ liệu: dữ liệu được sính ra từ đâu, hệ thống nào,… Trong trường hợp việc này là không thê, bạn đọc hãy thử giải mã đoạn văn bản bằng một số bộ mã hóa thường gặp cho đến khi gặp được kết quả mong muốn! Trong trường hợp dữ liệu ở trên do nguồn là tiếng Việt nên chúng ta có thể thử các

parse_character(x$A, locale = locale(encoding = "Latin2"))
## [1] "lę"     "táo"    "quýt"   "cŕ tím" "mít"

Kết quả khi sử dụng bộ mã \(Latin2\) đã cho một vài giá trị có ý nghĩa, chúng ta tiếp tục thử với \(Latin1\)

parse_character(x$A, locale = locale(encoding = "Latin1"))
## [1] "lê"     "táo"    "quýt"   "cà tím" "mít"

May mắn là cột dữ liệu đều đã có thể đọc được với chúng ta. Đối với cột \(B\) của dữ liệu bạn đọc có thể sử dụng parse_numbder() như đã trình bày ở trên. Dữ liệu sau khi được định dạng lại đã dễ hiểu hơn rất nhiều

tibble(Name = parse_character(x$A, locale = locale(encoding = "Latin1")), 
      Price = parse_number(x$B, locale = locale(grouping_mark = ".")))
## # A tibble: 5 × 2
##   Name   Price
##   <chr>  <dbl>
## 1 lê     20000
## 2 táo    35000
## 3 quýt   30000
## 4 cà tím  5500
## 5 mít    10000

5.3 Giá trị ngoại lai và giá trị không quan sát được.

Giá trị không quan sát được là các giá trị xuất hiện dưới dạng \(NA\) trong dữ liệu sau khi nhập vào R. Có nhiều lý do khác nhau dẫn đến việc dữ liệu không quan sát được, chẳng hạn như thông tin do người làm dữ liệu cung cấp không đầy đủ, những người cung cấp dữ liệu từ chối chia sẻ thông tin, hệ thống quản lý dữ liệu bị lỗi, hoặc cũng có thể do người quản lý xóa dữ liệu vì lý do bảo mật. Giá trị không quan sát được ngoài các giá trị \(NA\) xuất hiện trong dữ liệu, mà còn là các giá trị không phù hợp với kiểu dữ liệu hoặc miền giá trị của cột dữ liệu. Đối với một vài hệ thống khi dữ liệu được xuất ra giá trị không quan sát được vẫn được ghi nhận bằng một giá trị nào đó. Bạn đọc cần cẩn trọng khi làm việc với những dữ liệu kiểu như vậy.

Giá trị ngoại lai hay còn được gọi là giá trị bất thường là một điểm dữ liệu hoặc một quan sát sai khác đáng kể so với đa số các quan sát khác. Một giá trị ngoại lai xuất hiện trong dữ liệu có thể là do lỗi trong quản lý dữ liệu, do sai số trong đo lường hoặc cũng có thể do bản chất phân phối của dữ liệu. Tùy theo nguồn gôc của giá trị ngoại lai mà chúng ta có cách xử lý dữ liệu khác nhau.

Khi không được xử lý thích hợp, giá trị không quan sát được và các giá trị ngoại lai có thể làm sai lệch kết luận của tất cả các phân tích về dữ liệu, khiến người quản lý đưa ra quyết định sai lầm. Bạn đọc quan sát ví dụ sau:

## # A tibble: 4 × 6
##   MSV      Name            Age                Gender `Height (cm)` `Weight (kg)`
##   <chr>    <chr>           <chr>              <chr>          <dbl>         <dbl>
## 1 MSV00001 12345           30                 Nam             1.76            68
## 2 MSV43241 Nguyễn Văn An   Nhập sai ngày sinh N             169               72
## 3 MSV65432 Lê Thị Loan     -1                 Nữ            155               48
## 4 MSV34    Trần Mạnh Cường 15                 <NA>          175              150

Trong dữ liệu ở trên, mặc dù chỉ có 1 giá trị đang là \(NA\) ở cột giới tính nhưng nếu quan sát kỹ bạn đọc sẽ nhận ra rằng:

  • Cột \(name\): giá trị “12345” không thể là tên của một sinh viên, do đó đây cũng là một giá trị không quan sát được.

  • Cột \(Age\): thứ nhất, giá trị ở hàng thứ hai của cột \(Age\) là kiểu chuỗi ký tự. Thứ hai, tuổi của một sinh viên không thể là số âm, nên giá trị \(-1\) không phù hợp với miền giá trị của cột này. Cột \(Age\) có hai giá trị không quan sát được.

  • Cột \(Gender\): có giá trị là ký tự \(N\) không rõ là thể hiện cho giới tính Nam hay Nữ, giá trị này cũng là không quan sát được.

  • Cột \(MSV\): Giả sử bằng một cách nào đó, bạn đọc biết rằng mã sinh viên phải là một đoạn ký tự có độ dài là 8, bao gồm đoạn ký tự “MSV” và theo sau là 5 chữ số, thì giá trị “MS34” cũng là một giá trị không quan sát được ở cột mã sinh viên.

Để xác định dữ liệu có giá trị ngoại lai hay không cần sử dụng các kiến thức về thống kê toán:

  • Cột \(Height\) có giá trị chiều cao ở hàng thứ nhất là 1,76 cm. Giá trị này quá nhỏ để làm chiều cao của một người bình thường. Nhiều khả năng khi đo chiều cao của sinh viên, người nhập dữ liệu đã ghi lại theo đơn vị mét.

  • Cột \(Weight\) có giá trị cân nặng của hàng thứ 4 là 150 kg. Mặc dù dữ liệu có rất ít quan sát để đưa ra kết luận phân phối xác suất của cân nặng của sinh viên là gì, tuy nhiên với kiến thức thực tế chúng ta có thể kết luận rằng 150 kg là một cân nặng lớn bất thường với các giá trị cân nặng còn lại. Đây nhiều khả năng là một giá trị ngoại lai.

Để xác định các giá trị không quan sát được và giá trị ngoại lai tùy thuộc vào từng dữ liệu cụ thể và kiến thức tổng hợp và kiến thức chuyên môn của người xử lý dữ liệu và nằm ngoài phạm vi thảo luận của cuốn sách này. Dữ liệu ở trên chỉ là một dữ liệu nhỏ và đơn giản nên việc xác định các giá trị không quan sát được và giá trị ngoại lai là không khó. Chúng ta biến đổi các giá trị không quan sát được thành \(NA\) như sau

df$MSV[(nchar(df$MSV)!=8)]<-NA # mã sinh viên không có 8 ký tự là không quan sát được
df$Name[df$Name=="12345"]<-NA
df$Age<-parse_number(df$Age, na = c("-1")) # tuổi có giá trị (-1) là không quan sát được
df$Gender[df$Gender == "N"]<-NA

Đối với các giá trị ngoại lai, chúng ta sẽ đổi giá trị bị ghi nhận sai đơn vị về đúng đơn vị. Với giá trị cân nặng 150 kg, do dữ liệu nhỏ, bạn đọc có thể giữ nguyên giá trị này hoặc thay thế giá trị này bằng giá trị lớn nhất của những người có cân nặng thông thường.

df$Height[1]<-df$Height[1] * 100 # đổi đơn vị đo từ mét sang cm

Dữ liệu sau khi xử lý giá trị ngoại lai và định nghĩa lại các giá trị không quan sát được như sau:

df
## # A tibble: 4 × 6
##   MSV      Name              Age Gender `Height (cm)` `Weight (kg)`
##   <chr>    <chr>           <dbl> <chr>          <dbl>         <dbl>
## 1 MSV00001 <NA>               30 Nam             1.76            68
## 2 MSV43241 Nguyễn Văn An      NA <NA>          169               72
## 3 MSV65432 Lê Thị Loan        NA Nữ            155               48
## 4 <NA>     Trần Mạnh Cường    15 <NA>          175              150

Hàm số is.na(df) trả lại giá trị là \(TRUE\) nếu dữ liệu là quan sát là \(NA\) và trả lại giá trị \(FALSE\) nếu không phải \(NA\). Bạn đọc có thể dùng hàm is.na() kết hợp với hàm sum() để tính toán mỗi cột có bao nhiêu giá trị không quan sát được và tỷ lệ số giá trị không quan sát được trên mỗi cột là bao nhiêu:

sapply(df,function(x) sum(is.na(x))) # cho biết mỗi cột dữ liệu có bao nhiêu giá trị NA
##         MSV        Name         Age      Gender Height (cm) Weight (kg) 
##           1           1           2           2           0           0
sapply(df,function(x) sum(is.na(x))/length(x)) # cho biết tỷ lệ giá trị NA trong mỗi cột
##         MSV        Name         Age      Gender Height (cm) Weight (kg) 
##        0.25        0.25        0.50        0.50        0.00        0.00

Với những dữ liệu nhỏ thì hiển thị trực tiếp số lượng giá trị \(NA\) trong mỗi cột là hiệu quả nhất. Khi dữ liệu có nhiều quan sát và nhiều biến, bạn đọc nên sử dụng đồ thị để mô tả số lượng hoặc tỷ lệ giá trị không quan sát được của mỗi cột. Ví dụ như với dữ liệu \(gapminder\) của thư viện \(dslabs\), sử dụng đồ thị \(barplot\) để mô tả giá trị không quan sát được sẽ hiệu quả hơn:

y<-sapply(gapminder,function(x) sum(is.na(x))/length(x)*100) # cho biết tỷ lệ giá trị NA trong mỗi cột
barplot(sort(y), main = "Tỷ lệ giá trị không quan sát được",
        ylab = "Đơn vị %",
        xlab = "",
        col = "lightskyblue")

Xử lý giá trị không quan sát được dựa trên kinh nghiệm và hiểu biết về dữ liệu của bạn đọc luôn là ưu tiên trước tiên. Nếu chúng ta không có kinh nghiệm và hiểu biết về dữ liệu, các kỹ thuật xử lý dựa trên các nguyên tắc của xác suất thống kê sẽ được sử dụng.

5.3.1 Giá trị ngoại lai.

Giá trị ngoại lai, hay còn gọi là giá trị bất thường, là những giá trị mà khác xa tập hợp các giá trị còn lại. Không có một định nghĩa định lượng chính xác nào cho khái niệm như thế nào là khác xa các giá trị còn lại. Do đó, tùy theo bản chất của dữ liệu và tùy theo quan điểm của người phân tích dữ liệu mà một hay một số giá trị có khả năng là giá trị ngoại lai hay không. Giá trị ngoại lai thường chỉ được nhắc đến với các dữ liệu có số quan sát đủ lớn để đưa ra kết luận có ý nghĩa thống kê.

Khi dữ liệu có 10 quan sát như hình bên trái, có 8 quan sát màu xanh da trời nằm gần nhau hơn, điểm màu cam hơi xa hơn tập hợp các điểm màu xanh da trời một chút, còn điểm màu đỏ nằm xa hơn. Khi gặp dữ liệu như vậy, chúng ta có thể kết luận điểm màu đỏ là giá trị ngoại lai, còn kết luận điểm màu cam có phải ngoại lai hay không thì còn tùy thuộc vào ý tưởng của người phân tích dữ liệu. Chuyển sang hình bên phải với dữ liệu có 100 quan sát. Các điểm màu xanh da trời định hình khá rõ miền giá trị của trung tâm của dữ liệu là nằm xung quanh điểm (3,3). Chúng ta có thể kết luận một cách khá chắc chắn rằng điểm màu đỏ là một giá trị ngoại lai. Điểm màu cam, mặc dù nằm khá xa trung tâm của dữ liệu, nhưng để kết luận rằng có phải giá trị ngoại lai hay không vẫn phụ thuộc vào ý tưởng của người phân tích.

Nguồn gốc của giá trị ngoại lai là có thể có nhiều nguyên nhân khác nhau, bao gồm cả nguyên nhân khách quan hoặc nguyên nhân chủ quan. Các nguyên nhân khách quan có thể do nguồn sinh dữ liệu, hay hệ thống quản lý dữ liệu gặp sự cố, do lỗi trong quá trình truyền hoặc sao chép dữ liệu. Nguyên nhân chủ quan bao gồm có các hành vi gian lận, lỗi nhập và sao chép dữ liệu của con người, hoặc các giá trị được cố tình đưa vào trong dữ liệu với mục đích lấy phản hồi từ người dùng dữ liệu.

Nếu không xử lý giá trị ngoại lai kết quả tính toán sẽ bị sai lệch đáng kể. Dữ liệu có kích thước càng nhỏ thì ảnh hưởng của giá trị ngoại lai lại càng lớn. Trong ví dụ ở trên, giả sử bạn đọc cần phân tích sự tác động của biến \(X\) (trục nằm ngang) lên biến \(Y\) (giá trị trên trục dọc) bằng một mối quan hệ tuyến tính. Hãy quan sát kết quả của phân tích khi chúng ta

  1. Giữ nguyên 10 quan sát và phân tích mối liên hệ tuyến tính.

  2. Loại bỏ điểm A (màu đỏ) ra và phân tích mối liên hệ tuyến tính.

  3. Loại bỏ điểm A (màu đỏ) và điểm B (màu cam) ra để phân tích mối liên hệ tuyến tính.

Khi giữ nguyên 10 điểm dữ liệu để xây dựng mối quan hệ tuyến tính, đường thẳng mô tả mối quan hệ giữa \(Y\)\(X\) là nằm trong hình phía bên trái. Đường thẳng này có hệ số góc dương (một đường dốc lên khi đi từ trái sang phải), điều này có nghĩa là biến \(X\) có tác động cùng chiều lên biến \(Y\). Sau khi loại bỏ điểm A (màu đỏ) và tính toán lại, đường thẳng mô tả mối quan hệ tuyến tính giữa \(Y\)\(X\) ở là đường thẳng trong hình ở giữa. Đường thẳng gần như nằm ngang, cho thấy \(X\) ít có tác động lên biến \(Y\). Sau cùng, trong hình phía bên phải, sau khi loại bỏ các điểm A (màu đỏ) và điểm B (màu cam), đường thẳng mô tả mối quan hệ tuyển tính giữa \(Y\)\(X\) là đường dốc xuống, nghĩa là mối tác động của \(X\) lên \(Y\) là ngược chiều. Bạn đọc có thể thấy rằng kết luận đưa ra sau khi phân tích thay đổi hoàn toàn khi chúng ta có các lựa chọn khác nhau về loại bỏ các giá trị được cho là ngoại lai ra khỏi dữ liệu. Sự tác động của \(X\) lên \(Y\) từ thuận chiều (hình bên trái) chuyển sang không có mối liên hệ (hình ở giữa) và sau cùng là sự tác động ngược chiều của \(X\) lên \(Y\) (hình bên phải).

Trong phần tiếp theo chúng thôi sẽ thảo luận về các phương pháp dùng để xác định các giá trị ngoại lai trong dữ liệu.

5.3.2 Cách phát hiện giá trị ngoại lai

Không có một định nghĩa chính xác như thế nào là giá trị ngoại lai, chính vì thế không có phương pháp tổng thể và thống nhất nào để phát hiện giá trị ngoại lai trong dữ liệu. Với mỗi cách nhìn nhận giá trị ngoại lại khác nhau mà có phương pháp tiếp cận cụ thể để xác định các giá trị đó. Chúng tôi chỉ trình bày các phương pháp chung được chấp nhận rộng rãi bởi những người phân tích dữ liệu. Các phương pháp đơn giản sẽ được trình bày cụ thể ngay trong phần này. Các phương pháp phức tạp hơn đòi hỏi kiến thức của các chương sau của cuốn sách sẽ được trình bày dưới dạng ý tưởng và hàm có sẵn trong thư viện bổ sung.

5.3.2.1 Phát hiện giá trị ngoại lai trong một véc-tơ.

Để xác định một giá trị là giá trị ngoại lai hay không luôn bao gồm hai bước, bước thứ nhất là sử dụng các phương pháp xác suất thống kê để xác định các giá trị có nhiều khả năng là ngoại lai, và bước thứ hai là sử dụng kiến thức chuyên môn hoặc hỏi ý kiến chuyên gia (nếu có thể) để khẳng định lại kết quả từ bước thứ nhất.

Nếu véc-tơ là một véc-tơ kiểu chuỗi ký tự (không phải kiểu factor) thì không có quy tắc rõ ràng nào để xác định giá trị ngoại lai. Một chuỗi ký tự có thể là ngoại lai nếu chuỗi ký tự có độ dài bất thường, có chứa nhiều ký tự bất thường, một chuỗi ký tự không có ý nghĩa, hoặc cũng có thể là một chuỗi ký tự trống,… việc này hoàn toàn phụ thuộc vào cách tiếp cận của người phân tích dữ liệu. Các phương pháp xử lý dữ liệu kiểu chuỗi ký tự hiện đại có khả năng biến đổi một chuỗi ký tự thành một véc-tơ kiểu số. Việc xác định chuỗi ký tự có phải là một giá trị bất thường hay không sẽ liên quan đến việc xác định một véc-tơ kiểu số có phải là một véc-tơ có giá trị bất thường trong một tập hợp các véc-tơ. Các kỹ thuật này vượt quá phạm vi của cuốn sách

Đối với véc-tơ kiểu factor hay véc-tơ kiểu logic, giá trị có khả năng là ngoại lai là các giá trị xuất hiện với tần xuất rất nhỏ. Chẳng hạn như khi mô tả một véc-tơ chứa tên các loại đồ uống được bán trong một siêu thị trong tháng vừa rồi, bạn gặp trường hợp sau:

x<-sample(c("Coca","Pepsi","Red bull","Mirinda","Collagen"),10000,prob = c(4000,3000,2000,2000,5),replace = TRUE)
barplot(sort(table(x)/length(x)),col = "lightskyblue")

Khi gặp đồ thị như trên, có khả năng đồ uống có tên “Collagen” là giá trị ngoại lai. Nếu siêu thị có bán loại đồ uống này và việc sản phẩm không được khách hàng ưa chuộng, việc xuất hiện với tần xuất thấp là bình thướng và đây không phải là giá trị ngoại lai. Tuy nhiên việc tên sản phẩm xuất hiện trong danh sách bán hàng tháng này dù siêu thị không bán cũng có thể là do lỗi gặp phải trong quản lý hệ thống bán hàng khi đã ghi nhận tên “Collagen” cho một đồ uống khác.

Đối với véc-tơ kiểu số, các giá trị có khả năng là ngoại lai thường là các giá trị nằm ở đuôi của phân phối xác suất. Để biết một véc-tơ kiểu số có giá trị ngoại lai hay không, bạn đọc nên sử dụng đồ thị boxplot. Các điểm nằm phía dưới điểm nhỏ nhất (Q0) và nằm phía trên điểm lớn nhất (Q4) của đồ thị boxplot có nhiều khả năng là các giá trị ngoại lai. Điểm nhỏ nhất và điểm lớn nhất của đồ thị boxplot được xác định dựa trên mức từ phân vị thứ nhất (Q1) và mức tứ phân vị thứ 3 (Q3): \[\begin{align} &&\text{Inter Quartile Range (IQR)} = Q3 - Q1 \\ &&\text{Điểm nhỏ nhất (Q0)} = Q1 - 1.5 \times IQR \\ &&\text{Điểm lớn nhất (Q4)} = Q3 + 1.5 \times IQR \end{align}\]

Các giá trị nằm ngoài khoảng \((Q1 - 1,5 \times IQR, Q3 + 1.5 \times IQR)\) có nhiều khả năng là giá trị ngoại lai. Giá trị càng thấp hơn Q0 và càng cao hơn Q4 thì khả năng là giá trị ngoại lai lại càng cao.

Đồ thị boxplot dưới đây mô tả phân phối của véc-tơ chứa khối lượng giao dịch, tính bằng triệu cổ phiếu/ngày, của cổ phiếu của tập đoàn FLC. Cổ phiếu được niêm yết trên sàn giao dich chứng khoán Thành phố Hồ Chí Minh từ ngày 6 tháng 10 năm 2011 đến ngày 8 tháng 9 năm 2022. Dữ liệu có hơn quan sát.

dat1<-read_csv("../KHDL_KTKD/Dataset/FLC.csv")
dat1%>%filter(year(Date)>=2021)%>%ggplot(aes(y=Volume/10^6))+
  geom_boxplot(fill = "lightskyblue",alpha=0.5)+
  theme_minimal()+
  labs(title = "Khối lượng giao dịch cổ phiếu FLC",
              subtitle = "Đơn vị: Triệu cổ phiếu/ngày",
              caption = "Nguồn dữ liệu: Sở giao dịch chứng khoán TP HCM")+
  theme(legend.position="none")+theme(axis.text=element_text(size=12),
        axis.title=element_text(size=12,face="bold"))+
  ylab("")+xlab("")+
  theme(plot.title = element_text(size = 14, face = "bold"))

Chúng ta có thể thấy trên đồ thị boxplot không có điểm nằm dưới Q0. Có 8 quan sát có giá trị lớn hơn Q4; các giá trị này có khả năng là các giá trị ngoại lai. Có 3 quan sát với giá trị lớn hơn 100, nghĩa là có ba ngày mà có hơn 100 triệu cổ phiếu FLC được giao dịch. Nếu có một chút kinh nghiệm về thị trường chứng khoán Việt Nam, bạn đọc có thể kiểm chứng được đây là số lượng cổ phiếu giao dịch lớn bất thường.

Thực tế thì ba phiên giao dịch có khối lượng giao dịch lớn hơn 100 triệu cổ phiếu là các phiên giao dịch ngày 10 tháng 1 năm 2022, ngày 11 tháng 1 năm 2022 và phiên giao dịch ngày 1 tháng 4 năm 2022. Thực tế cho thấy đây là ba phiên giao dịch mà cổ phiếu FLC đã bị thao túng giá và dẫn đến việc cố phiếu FLC bị cấm giao dịch trên sàn giao dịch TP HCM kể từ tháng 09 năm 2022.

  • Sau một vài tháng giá cổ phiếu FCL tăng lên gấp 2 lần, đến ngày 10 và ngày 11 tháng 01 năm 2022, các cổ đông chính của FLC bán ra khối lượng rất lớn các cổ phiếu mà không đăng ký với Ủy ban chứng khoán theo quy định. Sau hai phiên giao dịch này giá cổ phiếu FLC giảm mạnh về đến mức trước đó vài tháng.

  • Ngày 31 tháng 03 năm 2022 các thông tin giả mạo về nhu cầu mua cổ phiếu FLC với khối lượng lớn được đưa ra (sau nhiều ngày giá cổ phiếu FLC giảm hết biên độ) làm cho nhu cầu mua FLC trong ngày 01 tháng 04 năm 2022 cao đột biến.

Đây là ví dụ điển hình về dữ liệu có giá trị ngoại lai có nguyên nhân chủ quan từ con người. Bạn đọc có thể sử dụng kết hợp đồ thị boxplot và các đồ thị mô tả phân phối của biến liên tục như đồ thị \(histogram\) hay đồ thị \(density\). Hình vẽ dưới đây mô tả phân phối của chiều cao của 245 nam giới là nhân viên của một công ty. Đơn vị đo chiều cao là \(cm\).

Đồ thị boxplot và histogram đều cho thấy trong dữ liệu có các giá trị chiều cao của nam giới xấp xỉ giá trị 0 và nhiều khả năng đây là các giá trị ngoại lai. Đồ thị histogram còn cho thấy có nhiều hơn 1 giá trị có giá trị như vậy. Lọc các giá trị đó ra khỏi véc-tơ chúng ta sẽ thu được 5 giá trị là 1,52; 1,74; 1,70; 1,62; và 1,80. Đây không thể là chiều cao của nam giới đo bằng đơn vị \(cm\). Có nhiều khả năng là khi ghi lại chiều cao của các nhân viên này, người nhập dữ liệu đã sử dụng đơn vị là \(mét\) thay vì \(cm\). Chúng ta có thể sửa các giá trị ngoại lai này bằng cách đổi từ đơn vị \(mét\) sang \(cm\). Phân phối của chiều cao sau khi sửa lại dữ liệu được mô tả như hình dưới đây:

Véc-tơ kiểu số có mô tả giá trị đo lường, tiền tệ rất thường xuyên gặp vấn đề như kể trên. Ngay khi gặp giá trị ngoại lai trong véc-tơ kiểu số như trên bạn đọc hãy nghĩ đến sai đơn vị đo lường là nguyên nhân đầu tiên.

Ngoài việc sử dụng các tứ phân vị để phát hiện giá trị ngoại lai, một phương pháp định lượng khác cũng thường được đề cập đến trong nhiều tài liệu là sử dụng \(Z-Score\). \(Z-Score\) được tính bằng khoảng cách từ 1 điểm đến giá trị trung bình của dữ liệu sau đó chia cho độ lệch chuẩn của dữ liệu \[\begin{align} Z-Score(x_i) = \cfrac{|x_i - \bar{x}|}{\sigma(x)} \end{align}\]

\(Z-Score\) dựa trên giả thiết là dữ liệu có phân phối chuẩn, do đó các điểm dữ liệu có \(Z-Score\) lớn, thường sử dũng ngưỡng lớn hơn 3, được coi là các giá trị ngoại lai. Chẳng hạn như khi vẽ \(Z-Score\) của tất cả các điểm dữ liệu trong dữ liệu về chiều cao cùa nhân viên, chúng ta sẽ có đồ thị như sau

Các điểm màu đỏ là các điểm bị ghi nhận sai đơn vị đo lường từ \(cm\) sang \(mét\) và có \(Z-Score\) lên đến hơn 6. Trong trường hợp này \(Z-Score\) cũng là phương pháp định lượng hiệu quả để xác định giá trị ngoại lai. Tuy nhiên, \(Z-Score\) có điểm bất lợi là giá trị này được tính toán dựa trên giá trị trung bình và độ lệch tiêu chuẩn của dữ liệu trong khi chính các giá trị đó lại phụ thuộc rất lớn vào các giá trị ngoại lai. Một cách đề giảm thiểu tác động của giá trị ngoại lai lên tính \(Z-Score\) là không tính đến \(x_i\) khi tính toán trung bình \(\bar{x}\)\(\sigma(x)\).

Đa số các phương pháp xác định giá trị ngoại lai ở trên đều dựa trên giả thiết là véc-tơ dữ liệu có phân phối chuẩn. Dữ liệu về bồi thường bảo hiểm là một điển hình của dữ liệu không có phân phối chuẩn. Đồ thì dưới đây mô tả số liệu về tiền bồi thường bảo hiểm sức khỏe của hơn 1.000 khách hàng của một công ty bảo hiểm nhân thọ

Nhiều điểm dữ liệu bị xác định là ngoại lai trong trường hợp này mặc dù đây là dữ liệu chính xác. Trong thực tế, nếu bạn đọc gặp dữ liệu không có phân phối chuẩn, hãy biến đổi dữ liệu về gần với phân phối chuẩn nhất có thể trước khi thực hiện các bước phân tích. Phép biến đổi thường được sử dụng nhất là biến đổi Box-Cox.

Kỹ thuật biến đổi Box-Cox được trình bày trong phụ lục của chương này.

5.3.2.2 Giá trị ngoại lai trong không gian nhiều chiều

Xác định giá trị ngoại lai trong không gian nhiều chiều phức tạp hơn trong không gian một chiều rất nhiều. Trong không gian một chiều, chúng ta cần xác định những số nào là giá trị ngoại lai của một véc-tơ. Trong khi trong không gian nhiều chiều, chúng ta cần phải xác định các quan sát nào là giá trị ngoại lai trong một dữ liệu. Ngoài việc xem xét giá trị trong từng cột dữ liệu, chúng ta cần phải xem xét cả mối liên hệ giữa các véc-tơ (cột) đó.

Các phương pháp để xác định giá giá trị ngoại lai trong không gian nhiều chiều vẫn dựa trên nguyên tắc cơ bản áp dụng trong không gian một chiều, đó là các quan sát càng xa điểm trung tâm của dữ liệu thì quan sát đó càng có khả năng cao là giá trị ngoại lai. Khái niệm xa hay gần trong một không gian nhiều chiều luôn gắn liền với một khái niệm về khoảng cách. Khoảng cách thường sử dung nhiều nhất trong không gian nhiều chiều là khoảng cách Euclid. Tuy nhiên khoảng cách Euclid có nhược điểm là không tính đến mối liên hệ giữa các cột dữ liệu. Khoảng cách thường được dùng trong xác định giá trị ngoại lai là khoảng cách Mahalanobis.

Cho \(x_i = x_{i1}, x_{i2}, \cdots, x_{ip}\) là quan sát thứ \(i\)\(\mu = \mu_{1}, \mu_{2}, \cdots, \mu_{p}\) là véc-tơ các giá trị trung bình của các véc-tơ cột. Khoảng cách Euclid và khoảng cách Mahalanobis được định nghĩa như sau \[\begin{align} D^{Euclid}(x_i,\mu) = \sqrt{(x_i - \mu)^T (x_i - \mu)} \\ D^{Mahalanobis}(x_i,\mu) = \sqrt{(x_i - \mu)^T \ \Sigma^{-1} \ (x_i - \mu)} \\ \end{align}\] trong đó \(D^{Euclid}(x_i,\mu)\)\(D^{Mahalanobis}(x_i,\mu)\) lần lượt là khoảng cách Euclid và khoảng cách Mahalanobis từ quan sát \(x_i\) đến điểm trung bình \(\mu\). Trong công thức tính khoảng cách Mahalanobis, \(\Sigma^{-1}\) là ma trận nghịch đảo của ma trận hiệp phương sai của dữ liệu. Ki Có thể thấy rằng khoảng cách Euclid là trường hợp riêng của khoảng cách Mahalanobis khi các cột dữ liệu có phương sai bằng 1 và đôi một độc lập với nhau.

Bạn đọc có thể tự lập trình hàm số tính khoảng cách Euclid và hàm số tính khoảng cách Mahalanobis giữa 2 véc-tơ như sau

Dis.Euc<-function(x,y) sum((x-y)^2)^0.5
Dis.Mah<-function(x,y,Sigma) (t(x-y)%*% solve(Sigma) %*%(x-y))^0.5 

Chúng ta quay trở lại ví dụ về dữ liệu bao gồm 10 quan sát với hai giá trị ngoại lai là điểm A (màu đỏ) và điểm B (màu cam). Chúng ta tính toán khoảng cách Euclid của mỗi điểm đến trung tâm của dữ liệu và sắp xếp các điểm theo thứ tự khoảng cách Euclid giảm dần

(#tab:unnamed-chunk-66)Điểm dữ liệu sắp xếp theo khoảng cách Euclid giảm dần
Điểm dữ liệu Tọa độ x Tọa độ y Khoảng cách Euclid Khoảng cách Mahalanobis
Điểm A (đỏ) 7.500 12.000 7.925 2.593
Điểm B (cam) 10.000 6.000 5.046 1.747
Điểm khác (xanh) 1.046 5.069 4.216 1.647
Điểm khác (xanh) 1.916 4.193 3.302 1.225
Điểm khác (xanh) 7.101 2.411 2.753 1.128
Điểm khác (xanh) 6.693 2.403 2.497 1.012
Điểm khác (xanh) 2.973 3.773 2.327 0.815
Điểm khác (xanh) 4.388 2.662 1.935 0.615
Điểm khác (xanh) 5.357 2.560 1.858 0.671
Điểm khác (xanh) 5.129 3.055 1.360 0.473

Có thế thấy rằng khi chỉ có 10 quan sát, khoảng cách Euclid có thể sử dụng để phát hiện được giá trị ngoại lai là điểm A và điểm B vì hai điểm này có khoảng cách đến trung tâm xa hơn so với các điểm còn lại. Khoảng cách Mahalanobis cũng cho kết quả tương tự. Tuy nhiên khoảng cách Euclid sẽ gặp vấn đề khi số lượng quan sát nhiều hơn và mối liên hệ giữa \(x\)\(y\) rõ ràng hơn. Bạn đọc có thể thấy khoảng cách Euclid không cho kết quả tốt như khoảng cách Malahanobis trong trường hợp dữ liệu có 100 quan sát

(#tab:unnamed-chunk-68)Điểm dữ liệu sắp xếp theo khoảng cách Euclid giảm dần
Điểm dữ liệu Tọa độ x Tọa độ y Khoảng cách Euclid Khoảng cách Mahalanobis
Điểm A (đỏ) 7.500 12.000 9.501 8.010
Điểm khác (xanh) -1.993 6.897 7.100 2.441
Điểm khác (xanh) -2.448 5.970 7.072 2.430
Điểm khác (xanh) 10.216 -0.163 7.011 2.377
Điểm khác (xanh) 9.862 -0.211 6.725 2.290
Điểm khác (xanh) 10.061 0.291 6.667 2.269
Điểm B (cam) 10.000 6.000 6.606 4.675
Điểm khác (xanh) 10.022 0.591 6.508 2.240
Điểm khác (xanh) 8.732 0.131 5.581 1.932
Điểm khác (xanh) -0.787 5.044 5.183 1.810

Khi đo bằng khoảng cách Euclid, điểm \(A\) vẫn là điểm xa trung tâm dữ liệu nhất. Tuy nhiên khoảng cách từ điểm \(B\) đến trung tâm dữ liệu là nhỏ hơn một số điểm màu xanh khác. Quan sát khoảng cách Mahalanobis chúng ta có thể thấy rằng điểm \(A\) là điểm có khoảng cách xa nhất, sau đó đến điểm \(B\) (Malahanobis distance = 4.675), các điểm màu xanh khác đều có khoảng cách Mahalanobis nhỏ hơn 2,5.

Các kỹ thuật phát hiện giá trị ngoại lai phức tạp hơn dựa trên nguyên lý phân cụm sẽ được trình bày trong chương “học máy không có giám sát”. Nguyên tắc xác định một quan sát ngoại lai là phân chia dữ liệu thành các cụm sao cho các quan sát trong cùng một cụm có tính chất tương tự nhau. Các quan sát không nằm trong cụm nào, hoặc trong các cụm có rất ít quan sát, có nhiều khả năng là giá trị ngoại lai.

5.3.3 Xử lý giá trị ngoại lai.

Có nhiều phương pháp để xử lý giá trị ngoại lai trong dữ liệu. Tùy thuộc vào tình huống và dữ liệu cụ thể, phương pháp nào cũng có thể đúng hoặc sai. Điều quan trọng là bạn đọc phải phân tích các tình huống có thể liên quan đến giá trị ngoại lai. Đôi khi việc phân tích các giá trị ngoại lai này còn giúp bạn có những hiểu biết hơn về dữ liệu và tối ưu công việc phân tích của bạn.

    1. Phương pháp đơn giản nhất và cũng thường cho hiệu quả thấp nhất đó là loại bỏ các quan sát, hoặc biến có chứa giá trị ngoại lai. Phương pháp này chỉ có ý nghĩa khi bạn có số lượng quan sát đủ lớn và các giá trị bị coi là ngoại lai không có có ý nghĩa trong xác định phân phối xác suất của từng biến.
    1. Thay thế giá trị ngoại lai bằng một giá trị khác: bạn đọc có thể thay thế giá trị ngoại lai bằng giá trị có ý nghĩa hơn như giá trị Q0 hoặc Q4 của phân phối xác suất, hoặc cũng có thể thay thế giá trị ngoại lai bằng giá trị trung bình, trung vị, hoặc mode của phân phối. Đây là phương pháp đơn giản, dễ sử dụng và thường cho hiệu quả tốt hơn so với phương pháp xóa quan sát.
    1. Phương pháp sau cùng và cũng là phương pháp đòi hỏi kỹ thuật phức tạp nhất đó là coi giá trị ngoại lai như một giá trị không quan sát được, sau đó xây dựng mô hình để dự đoán cho giá trị ngoại lai.

Các phương pháp thay thế giá trị ngoại lai và hoặc dự đoán giá trị ngoại lai dựa trên mô hình do tương tự như các phương pháp xử lý dữ liệu không quan sát được nên sẽ được trình bày ở phần sau của chương sách.

5.3.4 Xử lý giá trị không quan sát được.

Khi dữ liệu có giá trị không quan sát được, cách xử lý đơn giản nhất là xóa các quan sát hoặc xóa các biến chứa các giá trị đó. Nếu bạn đọc gặp dữ liệu mà trong đó có một hoặc một số quan sát mà đa số các giá trị trong đó là \(NA\), trong khi tất cả các quan sát còn lại đều không có chứa \(NA\), thì cách xử lý xóa đi quan sát có giá trị \(NA\) là giải pháp hợp lý nhất. Ví dụ như chúng ta có dữ liệu về thông tin của sinh viên của một lớp như sau

MSV Name Age Gender Height (cm) Weight (kg)
MSV00001 Lý Văn Thắng 30 Nam 176 68
MSV43241 Nguyễn Văn An 19 Nam 169 72
MSV65432 Lê Thị Loan 25 Nữ 155 48
MSV34001 Trần Mạnh Cường 15 Nam 175 150
MSV33789 Nguyễn Thị Thu Thủy NA NA NA NA

Quan sát tương ứng với mã sinh viên “MSV33789” ngoài thông tin về tên sinh viên, các thông tin khác đều không quan sát được. Ngoài sinh viên này, các sinh viên còn lại đều có đầy đủ thông tin. Trong trường hợp này phương pháp xử lý hiệu quả nhất là xóa sinh viên “MSV33789” khỏi dữ liệu trước khi phân tích.

Khi các giá trị không quan sát được tập trung ở một số quan sát (hàng) hoặc tập trung ở một số biến (cột), chúng ta nói rằng các giá trị không quan sát được một cách không ngẫu nhiên (Missing value not at random hay MNAR).

MSV Name Age Gender Height (cm) Weight (kg) GPA
MSV00001 Lý Văn Thắng 30 Nam 176 68 3.25
MSV43241 Nguyễn Văn An 19 Nam 169 72 NA
MSV65432 Lê Thị Loan 25 Nữ 155 48 NA
MSV34001 Trần Mạnh Cường 15 Nam 175 150 NA

Dữ liệu ở trên là ví dụ khác về việc giá trị không quan sát được xuất hiện một cách không ngẫu nhiên. Có thể thấy rằng trong cột \(GPA\) đa số các giá trị là không quan sát được. Mọi phân tích liên quan đến giá trị của biến này sẽ không có ý nghĩa, do đó cách tốt nhất là xóa cột này ra khỏi dữ liệu. Để xóa các quan sát có chứa giá trị không quan sát được ra khỏi dữ liệu, bạn đọc có thể sử dụng hàm drop_na() của thư viện tidyr. Để xóa một cột khỏi dữ liệu, bạn đọc có thể coi dữ liệu (một \(data.frame\) hoặc \(tibble\)) như là một \(list\) và gán giá trị của biến đó bằng \(NULL\)

# Du lieu là một tibble hoặc một data.frame có tên là dat
dat<-drop_na(dat) # Xóa các quan sát (hàng) có giá trị NA ra khỏi dữ liệu
dat$ten_cot<-NULL # xóa cột có tên là ten_cot ra khoi du lieu

Nếu giá trị không quan sát được tập trung vào một số quan sát thì xóa quan sát sẽ không làm ảnh hưởng đến kết quả phân tích. Dữ liệu chúng ta thường gặp sẽ có các giá trị không quán sát được nằm rải rác ở các cột không theo một quy tắc nào. Chúng tôi muốn nói đến trường hợp dữ liệu không quan sát được xuất hiện một cách hoàn toàn ngẫu nhiên (Missing completely at random hay MCAR). Khi gặp trường hợp này nếu xóa đi các quan sát có \(NA\), tỷ lệ quan sát bị xóa đi sẽ là đáng kể.

Để minh họa rõ hơn cho vấn đề này, và để đánh giá hiệu quả của các phương pháp xử lý giá trị \(NA\) trong các phần sau, chúng tôi sẽ sử dụng dữ liệu \(mpg\) của thư viện \(ggplot2\). Đây là dữ liệu có 234 quan sát và 11 biến. Dữ liệu mô tả mức độ tiêu hao nhiên liệu của các loại xe oto thương mại đang bán trên thị trường trong hai năm 1999 và 2008. Dữ liệu không có giá trị \(NA\) nhưng chúng ta sẽ thêm các giá trị không quan sát được vào dữ liệu một các ngẫu nhiên. Sau đó dữ liệu chính xác sẽ được sử dụng để đánh giá phương pháp xử lý giá trị không quan sát được. Bạn đọc cần đọc mô tả về dữ liệu \(mpg\), ý nghĩa của các biến sau đó sử dụng đoạn câu lệnh dưới đây để thêm giá trị \(NA\) vào trong dữ liệu một cách ngẫu nhiên. Chúng ta sẽ gọi tên dữ liệu mới là \(na.mpg\) để phân biệt với dữ liệu ban đầu.

# Tạo dữ liệu mới giống như dữ liệu ban mpg
na.mpg<-mpg

# Định dạng lại format các cột kiểu biến rời rạc thành kiểu factor
chiso<- !(names(na.mpg) %in% c("displ", "cty", "hwy"))
na.mpg[,chiso]<-lapply(na.mpg[,chiso], as.factor)%>%as.data.frame()

# Viết hàm số để thêm giá trị NA vào một véc-tơ
## hàm số có ý nghĩa là thêm vào véc-tơ x các giá trị NA một cách ngẫu nhiên
## tỷ lệ giá trị NA được thêm vào bằng tùy biến na.rate
rd.add<-function(x, na.rate){
  n<-length(x)
  k<-round(n*na.rate)
  ind<-sample(1:n,k,replace=FALSE)
  x[ind]<-NA
  return(x)
}

# Thêm giá trị NA vào các cột NGOẠI TRỪ ba cột
## Cột nhà sản xuất: manufacturer
## Cột loại xe: model
## Cột năm sản xuất
## tỷ lệ thêm NA một cách ngẫu nhiên vào các cột là 2%
chiso<- !(names(na.mpg) %in% c("manufacturer", "model", "year"))
set.seed(12)
na.mpg[,chiso]<-as.data.frame(lapply(na.mpg[,chiso], rd.add, na.rate = 0.02))

# Xem mỗi cột có bao nhiêu giá trị NA
sapply(na.mpg, f<-function(x) sum(is.na(x)))
## manufacturer        model        displ         year          cyl        trans 
##            0            0            5            0            5            5 
##          drv          cty          hwy           fl        class 
##            5            5            5            5            5

Chúng ta thấy rằng có 8/11 cột có giá trị \(NA\), cột có 5 giá trị \(NA\) xuất hiện một cách ngẫu nhiên trên tổng số 234 giá trị (tỷ lệ khoảng 2%). Tuy nhiên số quan sát có chứa \(NA\) lại lớn hơn 2% rất nhiều. Hàm drop_na() trong thư viện \(tidyr\) sẽ xóa các quan sát có giá trị \(NA\) ra khỏi dữ liệu. Chúng ta có thể tính được sau khi xóa tỷ lệ dữ liệu còn giữ lại là bao nhiêu:

nrow(drop_na(na.mpg))/nrow(na.mpg)
## [1] 0.8461538

Có thể thấy nếu 2% dữ liệu không quan sát được ở mỗi cột nếu chúng ta xóa các quan sát có giá trị \(NA\), tỷ lệ dữ liệu còn lại là khoảng 85%. Chúng ta có thể thử tăng tỷ lệ giá trị không quan sát được trên mỗi cột lên thành 3%, 5%, 10%, 20%, 30% và quan sát tỷ lệ dữ liệu còn lại sau khi xóa:

(#tab:unnamed-chunk-75)Tỷ lệ dữ liệu bị xóa nếu loại bỏ quán sát có NA
Tỷ lệ NA Tỷ lệ xóa Tỷ lệ còn lại
2% 15% 85%
3% 21% 79%
5% 34% 66%
10% 57% 43%
20% 84% 16%
30% 94% 6%

Chúng ta có thể thấy rằng xóa quan sát có giá trị \(NA\) không phải là một giải pháp hiệu quả khi giá trị \(NA\) xuất hiện trong hầu hết các cột. Từ kết quả trên có thể thấy rằng khi tỷ lệ \(NA\) là 5% trở lên ở mỗi cột trong số 8/11 cột của dữ liệu, chúng ta phải xóa đi khoảng 35% số quan sát để dữ liệu không còn \(NA\). Tỷ lệ dữ liệu xóa đi lớn như vậy sẽ ảnh hưởng lớn đến kết quả của phân tích.

Các phương pháp xử lý giá trị không quan sát được trong trường hợp này là thay thế giá trị \(NA\) bằng các giá trị thích hợp. Phương pháp đơn giản nhất đó là giả thiết các cột chứa giá trị không quan sát được độc lập với nhau và sử dụng các giá trị đặc trưng của cột dữ liệu tương ứng để thay thế cho giá trị \(NA\). Các phương pháp phức tạp hơn cân nhắc mối liên hệ giữa các cột dữ liệu và xây dựng các thuật toán để tìm giá trị tối ưu thay thế cho các giá trị không quan sát được nằm trong tất cả các cột. Mỗi phương pháp đều có ưu nhược điểm và chúng tôi thường thử cả hai hướng tiếp cận sau đó đánh giá hiệu quả dựa trên kết quả phân tích.

5.3.4.1 Thay thế giá trị không quan sát được bằng trung bình, trung vị hoặc mode.

Với giả thiết rằng cột chứa giá trị không quan sát được không có mối liên hệ đến các cột còn lại, chúng ta sẽ sử dụng một trong các giá trị như trung bình (mean), trung vị (median), hoặc mode của các giá trị quan sát được để thay thế cho các giá trị không quan sát được.

  • Giá trị trung bình thường được sử dụng để thay thế cho các giá trị không quan sát được cho véc-tơ kiểu số liên tục và phân phối của các giá trị không có đuôi dài.

  • Giá trị trung vị, là giá trị tại ngưỡng xác suất 50%, thường được sử dụng để thay thế cho các giá trị không quan sát được cho véc-tơ kiểu số liên tục và véc-tơ có đuôi dài. Giá trị trung vị có ưu điểm là ít bị ảnh hưởng bởi các giá trị ngoại lai và không bị thay đổi sau các bước biến đổi dữ liệu bằng các hàm đơn điệu.

  • Giá trị mode, là giá trị mà hàm mật độ có xác suất cao nhất, có thể dùng cho cả véc-tơ kiểu số liên tục hoặc véc-tơ kiểu biến rời rạc. Trong trường hợp véc-tơ kiểu số liên tục, bạn đọc cần phải ước lượng hàm mật độ nên giá trị mode sẽ còn phụ thuộc vào phương pháp tiếp cận của người phân tích.

Để thay thế giá trị không quan sát được bằng một giá trị khác, bạn đọc có thể sử dụng hàm na_if() của thư viện dplyr, hàm replace_na của thư viện tidyr, hoặc cũng có thể tự xây dựng hàm số của mình. Để đơn giản hóa, chúng ta giả sử rằng sẽ luôn luôn thay thế giá trị trung vị khi gặp véc-tơ kiểu số liên tục và giá trị mode khi gặp véc-tơ kiểu biến rời rạc.

my_mode<-function(x){ # tự định nghĩa hàm mode cho véc-tơ x
  names(which.max(table(x)))
}
my_fillna_1<-function(x){ # tự định nghĩa cách thay thế giá trị NA phương pháp thứ nhất
  if(is.numeric(x)){
    x[is.na(x)]<-median(x,na.rm=TRUE)
  } else {
    x[is.na(x)]<-my_mode(x)
  }
  return(x)
}
mpg_1<-lapply(na.mpg, my_fillna_1)%>%as.data.frame()
Giá trị thật của các biến kiểu số liên tục (nằm trong các cột \(displ\), \(hwy\), và \(cty\)) và giá trị dùng để thay thế được tổng kết lại trong bảng dưới đây:
(#tab:unnamed-chunk-78)Biến liên tục, thay thế NA bằng trung vị
displ đúng displ thay thế hwy đúng hwy thay thế cty đúng cty thay thế
4.0 3.3 16 25 17 17
5.4 3.3 24 25 11 17
3.8 3.3 12 25 15 17
2.7 3.3 27 25 18 17
1.8 3.3 20 25 26 17

Giá trị thật của các biến kiểu factor và giá trị dùng để thay thế được tổng kết trong bảng phía dưới

(#tab:unnamed-chunk-79)Biến rời rạc, thay thế NA bằng mode
cyl đúng cyl thay thế trans đúng trans thay thế drv đúng drv thay thế fl đúng fl thay thế class đúng class thay thế
4 4 auto(l6) auto(l4) f 4 e r suv suv
8 4 manual(m5) auto(l4) f 4 r r pickup suv
8 4 manual(m6) auto(l4) f 4 p r suv suv
4 4 auto(l4) auto(l4) 4 4 r r subcompact suv
6 4 manual(m6) auto(l4) f 4 p r pickup suv

Chúng ta có thể thấy rằng việc thay thế giá trị \(NA\) trong các véc_tơ kiểu biến liên tục bằng giá trị median là khá hiệu quả. Về tổng thể, giá trị median không cách quá xa so với giá trị thật.

Thay thế các giá trị không quan sát được trong các véc-tơ kiểu biến rời rạc bằng giá trị mode không cho kết quả tốt trong trường hợp này. Nguyên nhân là do giá trị mode trong các biến rời rạc không chiếm ưu thế so với các giá trị khác. Chẳng hạn như biến \(drv\) bị dự đoán sai 4/5 kết quả do biến này có 2 giá trị mode.

5.3.4.2 Thay thế giá trị không quan sát được bằng một mẫu ngẫu nhiên.

Vẫn với giả thiết rằng cột chứa giá trị không quan sát được không có mối liên hệ đến các cột còn lại, chúng ta sẽ sử dụng phép lấy mẫu ngẫu nhiên từ các giá trị quan sát được để thay thế cho các giá trị \(NA\). Hàm sample() là hàm số có sẵn trong R được sử dụng để sinh ngẫu nhiên. Để lấy ra \(k\) số ngẫu nhiên từ một véc-tơ \(x\) ban đầu, chúng ta viết câu lệnh như sau

sample(x,size = k, replace = TRUE) 

Tùy biến \(replace\) nhận giá trị bằng \(TRUE\) có ý nghĩa là giá trị ngẫu nhiên được lấy ra từ véc-tơ \(x\) có thể được lấy lặp lại. Chúng ta tự định nghĩa hàm fill_na_2() để thay thế giá trị ngẫu nhiên trong một véc-tơ \(x\) bằng các giá trị ngẫu nhiên được lấy từ \(x\) như sau

my_fillna_2<-function(x){ # tự định nghĩa cách thay thế giá trị NA, phương pháp thứ 2
  ind<-is.na(x) # véc-tơ kiểu logic, nhận giá trị TRUE tại các vị trí NA
  k<-sum(ind)
  x[ind]<-sample(x[!ind],k,replace = TRUE)
  return(x)
}
set.seed(12)
mpg_1<-lapply(na.mpg, my_fillna_2)%>%as.data.frame()
Giá trị thật của các biến kiểu số liên tục (nằm trong các cột \(displ\), \(hwy\), và \(cty\)) và giá trị dùng để thay thế được tổng kết lại trong bảng dưới đây:
(#tab:unnamed-chunk-82)Biến liên tục, thay thế NA bằng lấy mẫu ngẫu nhiên
displ đúng displ thay thế hwy đúng hwy thay thế cty đúng cty thay thế
4.0 4.7 16 17 17 17
5.4 4.0 24 14 11 21
3.8 4.0 12 19 15 15
2.7 4.0 27 19 18 18
1.8 4.0 20 17 26 13

Có thể thấy rằng biến \(year\) là biến gần như không có mối liên hệ đến các biến khác. Khi chúng ta xây dựng mô hình trên dữ liệu, việc xóa bỏ các biến không cần thiết ra khỏi mô hình là rất quan trọng vì các biến này có thể gây ra nhiễu cho mô hình và giảm khả năng dự đoán.

Còn quá sớm để nói đến xây dựng mô hình trên dữ liệu \(mpg\) như thế nào. Bạn đọc nên hiểu

Giá trị thật của các biến kiểu factor và giá trị dùng để thay thế được tổng kết trong bảng phía dưới

(#tab:unnamed-chunk-83)Biến rời rạc, thay thế NA bằng lấy mẫu ngẫu nhiên
cyl đúng cyl thay thế trans đúng trans thay thế drv đúng drv thay thế fl đúng fl thay thế class đúng class thay thế
4 4 auto(l6) manual(m5) f f e r suv compact
8 8 manual(m5) auto(l4) f f r r pickup compact
8 4 manual(m6) auto(l5) f f p p suv pickup
4 6 auto(l4) auto(l4) 4 f r r subcompact midsize
6 6 manual(m6) auto(l5) f f p r pickup midsize

Hiệu quả của phương pháp lấy mẫu ngẫu nhiên so với phương pháp sử dụng các giá trị trung vị hoặc mode là không rõ ràng. Tuy nhiên phương pháp này có nhược điểm lớn nhất đó là giá trị được sử dụng để thay thế là ngẫu nhiên nên có khả năng sẽ làm cho dữ liệu bị sai lệch.

5.3.4.3 Thay thế giá trị không quan sát được bằng cách xây dựng mô hình.

Giả thiết cột chứa giá trị không quan sát được không có mối liên hệ đến các cột còn lại là một giả thiết không thực tế. Các cột dữ liệu luôn luôn ít nhiều có mối liên hệ với nhau, hay nói theo khái niệm của xác suất - thống kê thì giá trị trong các cột dữ liệu thường không độc lập với nhau (Not independent hoặc dependent). Làm thế nào để biết hai cột dữ liệu bất kỳ là độc lập hay phụ thuộc là một câu hỏi không dễ có câu trả lời. Do đây là một vấn đề khó và vượt quá phạm vi của cuốn sách nên chúng tôi chỉ trình bày các phương pháp được công nhận rộng rãi. Để kiểm tra hai cột dữ liệu có độc lập hay không, bạn đọc hãy sử dụng các kiểm định như sau:

  1. Kiểm định Khi-bình phương khi cả hai biến đều là biến rời rạc.

  2. Kiểm định hệ số tương quan Person, hệ số tương quan Spearman, và hệ số tương quan Kendall khi cả hai biến đều là biến liên tục.

  3. Sử dụng phân tích phương sai (hay còn gọi là anova test) trong trường hợp một biến là rời rạc và một biến là liên tục.

Chi tiết của các kiểm định này được trình bày ở phần phụ lục của chương.

Để thực hiện kiểm định Khi-bình phương trong R, chúng ta sử dụng hàm chisq.test(). Để kiểm ra hai biến \(year\)\(drv\) có mối liên hệ hay không, chúng ta thực hiện như sau

chisq.test(na.mpg$year,na.mpg$drv)
## 
##  Pearson's Chi-squared test
## 
## data:  na.mpg$year and na.mpg$drv
## X-squared = 1.689, df = 2, p-value = 0.4298

Giá trị \(p-value\) bằng 42% nghĩa là xác suất bác bỏ giả thiết hai biến \(year\)\(drv\) độc lập là \(100\% - 42\% = 58\%\). Thông thường, mức xác suất bác bỏ giả thiết độc lập thường được chọn ở mức \(95\%\) hoặc thậm chí \(99\%\). Do xác suất bác bỏ giả thiết độc lập là thấp nên trong trường hợp này có thể đưa ra kết luận rằng hai biến \(year\)\(drv\) là không liên quan đến nhau. Tương tự, để kiểm ra hai biến \(drv\)\(cyl\) có mối liên hệ hay không, chúng ta thực hiện như sau

chisq.test(na.mpg$drv, na.mpg$cyl)
## 
##  Pearson's Chi-squared test
## 
## data:  na.mpg$drv and na.mpg$cyl
## X-squared = 90.288, df = 6, p-value < 2.2e-16

Do xác suất bác bỏ giả thiết độc lập (\(1-10^{-16}\)) là xấp xỉ \(100\%\) nên trong trường hợp này có thể đưa ra kết luận rằng hai biến \(drv\)\(cyl\) là không độc lập.

Để kiểm định hệ số tương quan giữa hai biến liên tục chúng ta sử dụng hàm cor.test(). Tùy biến \(method\) nhận giá trị “pearson”, “kendall”, hoặc “spearman” tương ứng với các hệ số tương quan Pearson, hệ số tương quan Kendall hoặc hệ số tương quan Spearman. Chúng ta kiểm định sự độc lập giữa hai biến \(displ\)\(hwy\) như sau

cor.test(na.mpg$displ, na.mpg$hwy, method = "pearson")
## 
##  Pearson's product-moment correlation
## 
## data:  na.mpg$displ and na.mpg$hwy
## t = -17.743, df = 223, p-value < 2.2e-16
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
##  -0.8143893 -0.7048317
## sample estimates:
##       cor 
## -0.765092
cor.test(na.mpg$displ, na.mpg$hwy, method = "kendall")
## 
##  Kendall's rank correlation tau
## 
## data:  na.mpg$displ and na.mpg$hwy
## z = -13.857, p-value < 2.2e-16
## alternative hypothesis: true tau is not equal to 0
## sample estimates:
##        tau 
## -0.6534741
cor.test(na.mpg$displ, na.mpg$hwy, method = "spearman")
## 
##  Spearman's rank correlation rho
## 
## data:  na.mpg$displ and na.mpg$hwy
## S = 3467012, p-value < 2.2e-16
## alternative hypothesis: true rho is not equal to 0
## sample estimates:
##        rho 
## -0.8262809

Kiểm định cả ba hệ số tương quan đều cho xác suất bác bỏ giả thiết hai biến độc lập là xấp xỉ 100%. Nói một cách khác có thể khẳng định hai biến \(displ\)\(hwy\) là có sự phụ thuộc.

Sau cùng, để kiểm định sự phụ thuộc giữa một biến rời rạc và một biến liên tục, chúng ta sử dụng phân tích phương sai. Hàm số để thực hiện phân tích phương sai trong R là hàm aov(). Chúng ta kiểm định sự phụ thuộc giữa \(hwy\)\(cyl\) như sau

summary(aov(hwy~cyl,data=na.mpg))
##              Df Sum Sq Mean Sq F value Pr(>F)    
## cyl           3   4479  1492.9   101.6 <2e-16 ***
## Residuals   220   3233    14.7                   
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 10 observations deleted due to missingness

Xác suất bác bỏ giả thiết giá trị trung bình của biến \(hwy\) bằng nhau theo các nhóm của biến \(cyl\) là xấp xỉ \(100\%\) hay nói một cách khác \(hwy\)\(cyl\) là có mối liên hệ.

Để xem xét một cách tổng thể mối liên hệ giữa các biến trong dữ liệu \(na.mpg\), bạn đọc có thể sử dụng kiểm định phù hợp với từng cặp biến và lưu xác suất bác bỏ giả thiết độc lập vào một ma trận. Hàm số ind_check() được tự xây dựng có đầu vào là một dữ liệu (một tibble hoặc một data.frame) và đầu ra là một ma trận cho biết xác suất bác bỏ giả thiết độc lập của từng cặp biến như thế nào. Bạn đọc có thể xem câu lệnh của hàm số này ở phần phụ lục của chương.

Ma trận thể hiện xác suất bác bỏ giả thiết độc lập giữa từng cặp biến trong dữ liệu \(na.mpg\) như sau

(#tab:unnamed-chunk-88)Biến rời rạc, thay thế NA bằng lấy mẫu ngẫu nhiên
manufacturer model displ year cyl trans drv cty hwy fl class
manufacturer 1.00 1 1.00 0.13 1.00 1 1.00 1.00 1 1.00 1.00
model 1.00 1 1.00 0.00 1.00 1 1.00 1.00 1 1.00 1.00
displ 1.00 1 1.00 0.96 1.00 1 1.00 1.00 1 0.92 1.00
year 0.13 0 0.96 1.00 0.99 1 0.57 0.41 0 1.00 0.01
cyl 1.00 1 1.00 0.99 1.00 1 1.00 1.00 1 0.84 1.00
trans 1.00 1 1.00 1.00 1.00 1 1.00 1.00 1 1.00 1.00
drv 1.00 1 1.00 0.57 1.00 1 1.00 1.00 1 0.32 1.00
cty 1.00 1 1.00 0.41 1.00 1 1.00 1.00 1 1.00 1.00
hwy 1.00 1 1.00 0.00 1.00 1 1.00 1.00 1 1.00 1.00
fl 1.00 1 0.92 1.00 0.84 1 0.32 1.00 1 1.00 1.00
class 1.00 1 1.00 0.01 1.00 1 1.00 1.00 1 1.00 0.00

Có thể thấy biến \(year\) gần như không có mối liên hệ đến các biến khác. Khi xây dựng mô hình trên dữ liệu, sự xuất hiện của các biến không cần thiết sẽ thêm nhiễu và làm giảm chất lượng của mô hình. Trong trường hợp này, chúng ta sẽ loại bỏ biến \(year\) trước khi thực hiện dự đoán cho các giá trị không quan sát được.

Phương pháp để xây dựng mô hình dự đoán cho các giá trị không biết là sử dụng thuật toán “rừng ngẫu nhiên”. Đây là một thuật toán nâng cao của mô hình dạng cây quyết định. Còn quá sớm để nói về mô hình này, bạn đọc chỉ cần hiểu rằng chúng ta sẽ dựa vào các giá trị quan sát được để xây dựng mô hình (một hàm số \(f\)) mà biến có giá trị \(NA\) phụ thuộc vào biến không có giá trị \(NA\) để đưa ra dự đoán. Thư viện “missForest” hỗ trợ chúng ta làm việc này. Bạn đọc có thể cài thư viện sau đó sử dụng hàm missForest(). Quy trình điền giá trị \(NA\) vào dữ liệu \(na.mpg\) chỉ cần 1 dòng lệnh!

library(missForest)
### Thời gian chạy mất khoảng 1-2 phút
model<-missForest(select(na.mpg,-year), maxiter = 200, ntree = 100) 
mpg_1<-model$ximp # Dữ liệu mpg_1 là dữ liệu sau khi thay thế NA

Giá trị thật của các biến kiểu số và các giá trị thay thế trong bảng dưới đây

(#tab:unnamed-chunk-90)Biến liên tục, thay thế NA bằng giá trị dự đoán bằng random forest
displ đúng displ thay thế hwy đúng hwy thay thế cty đúng cty thay thế
4.0 3.999844 16 15.87147 17 16.37833
5.4 4.672718 24 24.43083 11 10.99517
3.8 3.788511 12 13.57500 15 14.23717
2.7 2.945000 27 25.85814 18 17.97967
1.8 1.929000 20 19.25812 26 25.90011

Giá trị thật của các biến kiểu factor và giá trị dùng để thay thế được tổng kết trong bảng phía dưới

(#tab:unnamed-chunk-91)Biến rời rạc, thay thế NA bằng giá trị dự đoán bằng random forest
cyl đúng cyl thay thế trans đúng trans thay thế drv đúng drv thay thế fl đúng fl thay thế class đúng class thay thế
4 4 auto(l6) auto(l5) f f e r suv suv
8 8 manual(m5) manual(m5) f f r r pickup pickup
8 8 manual(m6) auto(l4) f f p r suv suv
4 4 auto(l4) auto(l5) 4 4 r r subcompact subcompact
6 6 manual(m6) auto(s6) f f p r pickup pickup

Bạn đọc có thể thấy rằng ngoài biến \(trans\), đa số các biến còn lại đều được dự đoán khá chính xác bằng thuật toán rừng ngẫu nhiên. Chúng ta sẽ thảo luận về mô hình này trong chương thảo luận về mô hình cây quyết định.

5.4 Phụ lục

5.4.1 Box-Cox transformation

Biến đổi Box-Cox là một phương pháp biến đổi để đưa một véc-tơ có phân phối khác với phân phối chuẩn thành một véc-tơ có phân phối gần với phân phối chuẩn. Phép biến đổi Box-Cox chỉ có duy nhất một tham số \(\lambda\).

\[\begin{align} \end{align}\]

5.4.2 Kolmogorov-Smirnov tests

5.4.3 Pearson’s Chi-squared tests

5.5 Bài tập

6 Biến đổi dữ liệu bằng thư viện \(dplyr\)

Dữ liệu trước khi đưa vào trực quan hóa hoặc xây dựng mô hình hiếm khi có được định dạng chính xác mà bạn đọc mong muốn. Thông thường, bạn đọc sẽ cần tạo thêm một số biến, hoặc có thể bạn đọc chỉ muốn đổi tên các biến, hoặc sắp xếp lại các quan sát, hoặc tổng hợp dữ liệu để làm cho dữ liệu dễ dàng xử lý tiếp. Bạn đọc sẽ học cách thực hiện tất cả những phép biến đổi đó trong phần này. Nội dung chủ yếu của phần này sẽ hướng dẫn bạn đọc cách chuyển đổi dữ liệu của mình bằng cách sử dụng thư viện \(dplyr\) và tập dữ liệu \(gapminder\) - dữ liệu về sức khỏe và thu nhập của các quốc gia trên thế giới từ năm 1960 đến năm 2016.

Thư viện \(dplyr\) là một thư viện nằm trong thư viện tổng hợp \(tidyverse\), bạn đọc có thể gọi thư viện \(tidyverse\) hoặc trực tiếp thư viện \(dplyr\) lên cửa sổ đang làm việc. Bạn đọc cũng nên lưu ý rằng trong thư viện \(dplyr\) có một số hàm trùng tên với các hàm có sẵn trong R, chẳng hạn như hàm \(filter()\) hoặc hàm \(lag()\). Tuy nhiên R ưu tiên \(dplyr\) trước các thư viện có sẵn nên bạn đọc sẽ nhận được thông báo khi gọi thư viện này.

Dữ liệu \(gapminder\) là dữ liệu đã được sử dụng trong phần tiền xử lý dữ liệu. Bạn đọc lưu ý thực hiện các bước tiền xử lý dữ liệu trước khi chạy các câu lệnh biến đổi dữ liệu. Mỗi phần tiếp theo sẽ giới thiệu một hàm quan trọng của thư viện \(dplyr\) và các tham số của hàm đó. Sau cùng chúng tôi sẽ giới thiệu về cách sử dụng pipe (\(%\>\%\)) để kết nối các hàm với nhau trong một câu lệnh duy nhất.

6.1 Thêm biến bằng hàm \(mutate()\)

Khi bạn đọc muốn thêm các cột khác vào một dữ liệu được tính toán từ các cột hiện có, bạn đọc có thể sử dụng hàm \(mutate()\). Hàm \(mutate()\) luôn thêm cột vào sau cột cuối cùng trong dữ liệu hiện có. Khi bạn muốn thêm cột vào một vị trí cụ thể, bạn có thể sử dụng tùy biến

  • \(.after = ten\_cot\) trong đó \(ten\_cot\) là tên cột phía trước cột mà bạn đọc muốn thêm vào.
  • \(.before = ten\_cot\) trong đó \(ten\_cot\) là tên cột phía sau cột mà bạn đọc muốn thêm vào.
mytib<-as.tibble(gapminder) # mytib là dữ liệu kiểu tibble
mutate(mytib, gdp_per_capita = gdp/population) # thêm cột có tên là gdp_per_capita
## # A tibble: 10,545 × 10
##    country  year infan…¹ life_…² ferti…³ popul…⁴      gdp conti…⁵ region gdp_p…⁶
##    <fct>   <int>   <dbl>   <dbl>   <dbl>   <dbl>    <dbl> <fct>   <fct>    <dbl>
##  1 Albania  1960   115.     62.9    6.19  1.64e6 NA       Europe  South…     NA 
##  2 Algeria  1960   148.     47.5    7.65  1.11e7  1.38e10 Africa  North…   1243.
##  3 Angola   1960   208      36.0    7.32  5.27e6 NA       Africa  Middl…     NA 
##  4 Antigu…  1960    NA      63.0    4.43  5.47e4 NA       Americ… Carib…     NA 
##  5 Argent…  1960    59.9    65.4    3.11  2.06e7  1.08e11 Americ… South…   5254.
##  6 Armenia  1960    NA      66.9    4.55  1.87e6 NA       Asia    Weste…     NA 
##  7 Aruba    1960    NA      65.7    4.82  5.42e4 NA       Americ… Carib…     NA 
##  8 Austra…  1960    20.3    70.9    3.45  1.03e7  9.67e10 Oceania Austr…   9393.
##  9 Austria  1960    37.3    68.8    2.7   7.07e6  5.24e10 Europe  Weste…   7415.
## 10 Azerba…  1960    NA      61.3    5.57  3.90e6 NA       Asia    Weste…     NA 
## # … with 10,535 more rows, and abbreviated variable names ¹​infant_mortality,
## #   ²​life_expectancy, ³​fertility, ⁴​population, ⁵​continent, ⁶​gdp_per_capita

Câu lệnh trên thêm cột có tên là \(gdp\_per\_capita\) được tính bằng tổng thu nhập quốc nội (\(gdp\)) chia cho dân số của quốc gia đó. Nếu bạn đọc muốn cột mới được thêm vào ngay sau cột \(gdp\), hãy sử dụng thêm tùy biến \(.after\)

mutate(mytib, gdp_per_capita = gdp/population,.after = gdp) # thêm cột có tên là gdp_per_capita
## # A tibble: 10,545 × 10
##    country  year infan…¹ life_…² ferti…³ popul…⁴      gdp gdp_p…⁵ conti…⁶ region
##    <fct>   <int>   <dbl>   <dbl>   <dbl>   <dbl>    <dbl>   <dbl> <fct>   <fct> 
##  1 Albania  1960   115.     62.9    6.19  1.64e6 NA           NA  Europe  South…
##  2 Algeria  1960   148.     47.5    7.65  1.11e7  1.38e10   1243. Africa  North…
##  3 Angola   1960   208      36.0    7.32  5.27e6 NA           NA  Africa  Middl…
##  4 Antigu…  1960    NA      63.0    4.43  5.47e4 NA           NA  Americ… Carib…
##  5 Argent…  1960    59.9    65.4    3.11  2.06e7  1.08e11   5254. Americ… South…
##  6 Armenia  1960    NA      66.9    4.55  1.87e6 NA           NA  Asia    Weste…
##  7 Aruba    1960    NA      65.7    4.82  5.42e4 NA           NA  Americ… Carib…
##  8 Austra…  1960    20.3    70.9    3.45  1.03e7  9.67e10   9393. Oceania Austr…
##  9 Austria  1960    37.3    68.8    2.7   7.07e6  5.24e10   7415. Europe  Weste…
## 10 Azerba…  1960    NA      61.3    5.57  3.90e6 NA           NA  Asia    Weste…
## # … with 10,535 more rows, and abbreviated variable names ¹​infant_mortality,
## #   ²​life_expectancy, ³​fertility, ⁴​population, ⁵​gdp_per_capita, ⁶​continent

Một biến thể khác của hàm \(mutate()\)\(transmute()\). Hàm \(transmute()\) khác \(mutate()\) ở chỗ là chỉ giữ lại các cột mới được tạo thành

transmute(mytib, gdp_per_capita = gdp/population) # data mới chỉ có cột gdp_per_capita
## # A tibble: 10,545 × 1
##    gdp_per_capita
##             <dbl>
##  1            NA 
##  2          1243.
##  3            NA 
##  4            NA 
##  5          5254.
##  6            NA 
##  7            NA 
##  8          9393.
##  9          7415.
## 10            NA 
## # … with 10,535 more rows

6.2 Lựa chọn cột bằng hàm \(select()\)

Khi dữ liệu có quá nhiều cột và bạn đọc chỉ muốn sử dụng một số cột nhất định để phân tích, bạn đọc hãy sửa dụng hàm \(select()\). Điều quan trọng nhất khi sử dụng hàm \(select()\) là bạn đọc cần phải gọi tên đúng các cột (biến) mà mình muốn lựa chọn.

select(mytib, year, gdp, population) # lấy ra các cột year, gdp, population
## # A tibble: 10,545 × 3
##     year          gdp population
##    <int>        <dbl>      <dbl>
##  1  1960           NA    1636054
##  2  1960  13828152297   11124892
##  3  1960           NA    5270844
##  4  1960           NA      54681
##  5  1960 108322326649   20619075
##  6  1960           NA    1867396
##  7  1960           NA      54208
##  8  1960  96677859364   10292328
##  9  1960  52392699681    7065525
## 10  1960           NA    3897889
## # … with 10,535 more rows

Hàm \(select\) cũng có thể được sử dụng để thay đổi tên cột. Chẳng hạn bạn đọc muốn tên các cột mới tương ứng là \("Year"\), \("GDP"\)\("Population"\)

select(mytib, Year = year, Gdp =  gdp,  Population = population) # lấy ra và đổi tên các cột year, gdp, population
## # A tibble: 10,545 × 3
##     Year          Gdp Population
##    <int>        <dbl>      <dbl>
##  1  1960           NA    1636054
##  2  1960  13828152297   11124892
##  3  1960           NA    5270844
##  4  1960           NA      54681
##  5  1960 108322326649   20619075
##  6  1960           NA    1867396
##  7  1960           NA      54208
##  8  1960  96677859364   10292328
##  9  1960  52392699681    7065525
## 10  1960           NA    3897889
## # … with 10,535 more rows

Khi dữ liệu có quá nhiều cột và việc gọi tên chính xác các cột làm cho câu lệnh \(select()\) quá dài, bạn đọc có thể lựa chọn các cột đứng liền nhau bằng cách sau

select(mytib, year, gdp:population) # 
## # A tibble: 10,545 × 3
##     year          gdp population
##    <int>        <dbl>      <dbl>
##  1  1960           NA    1636054
##  2  1960  13828152297   11124892
##  3  1960           NA    5270844
##  4  1960           NA      54681
##  5  1960 108322326649   20619075
##  6  1960           NA    1867396
##  7  1960           NA      54208
##  8  1960  96677859364   10292328
##  9  1960  52392699681    7065525
## 10  1960           NA    3897889
## # … with 10,535 more rows

Câu lệnh trên có ý nghĩa là lấy ra cột \(year\) và tất cả các cột nằm giữa cột \(gdp\) và cột \(population\).

Bạn đọc không nhớ chính xác tên cột muốn lấy ra thì có thể sử dụng các tùy chọn sau:

  • \(starts\_with()\): lấy ra các cột có tên bắt đầu bằng một chuỗi ký tự nào đó.

  • \(ends\_with()\): lấy ra các cột có tên kết thúc bằng một chuỗi ký tự nào đó.

  • \(contains()\): lấy ra các cột có tên chứa một chuỗi ký tự nào đó.

  • \(matches()\): lấy ra các cột có tên khớp với một xxxxxxxxxxxxxxxxxxx nào đó

mytib1<-mutate(mytib, gdp_per_capita = gdp/population) # mytib1 là tibble có thêm cột tên là gdp_per_capita
select(mytib1, contains("gdp")) # lấy ra tất cả các cột có tên chứa "gdp"
## # A tibble: 10,545 × 2
##             gdp gdp_per_capita
##           <dbl>          <dbl>
##  1           NA            NA 
##  2  13828152297          1243.
##  3           NA            NA 
##  4           NA            NA 
##  5 108322326649          5254.
##  6           NA            NA 
##  7           NA            NA 
##  8  96677859364          9393.
##  9  52392699681          7415.
## 10           NA            NA 
## # … with 10,535 more rows

Hàm \(select()\) ngoài ý nghĩa là lựa chọn cột còn có ý nghĩa là loại bỏ một số cột nào đó khỏi dữ liệu hiện thời. Để loại bỏ cột, bạn đọc chỉ cần thêm dấu “-” vào trước tên cột.

select(mytib1, - starts_with("gdp")) # bỏ đi tất cả các cột có tên chứa "gdp"
## # A tibble: 10,545 × 8
##    country              year infant_mor…¹ life_…² ferti…³ popul…⁴ conti…⁵ region
##    <fct>               <int>        <dbl>   <dbl>   <dbl>   <dbl> <fct>   <fct> 
##  1 Albania              1960        115.     62.9    6.19  1.64e6 Europe  South…
##  2 Algeria              1960        148.     47.5    7.65  1.11e7 Africa  North…
##  3 Angola               1960        208      36.0    7.32  5.27e6 Africa  Middl…
##  4 Antigua and Barbuda  1960         NA      63.0    4.43  5.47e4 Americ… Carib…
##  5 Argentina            1960         59.9    65.4    3.11  2.06e7 Americ… South…
##  6 Armenia              1960         NA      66.9    4.55  1.87e6 Asia    Weste…
##  7 Aruba                1960         NA      65.7    4.82  5.42e4 Americ… Carib…
##  8 Australia            1960         20.3    70.9    3.45  1.03e7 Oceania Austr…
##  9 Austria              1960         37.3    68.8    2.7   7.07e6 Europe  Weste…
## 10 Azerbaijan           1960         NA      61.3    5.57  3.90e6 Asia    Weste…
## # … with 10,535 more rows, and abbreviated variable names ¹​infant_mortality,
## #   ²​life_expectancy, ³​fertility, ⁴​population, ⁵​continent

6.3 Lọc quan sát bằng hàm filter()

Hàm filter() cho phép bạn đọc lọc các quan sát dựa trên giá trị của các cột. Do có một số thư viện trong R sử dụng hàm \(filter()\) với mục đích khác nhau và chúng tôi không chắc chắn cửa sổ R bạn đọc đang sử dụng đang có sẵn các thư viện nào nên chúng tôi đổi tên cho hàm \(filter()\) của thư viện \(dplyr\) thành \(dfilter()\) phương pháp mượn tham số:

dfilter<-function(...) dplyr::filter(...)

Cách hoạt động của hàm \(filter()\) như sau: bạn đọc muốn lấy dữ liệu của năm 2010 từ dữ liệu \(gapminder\), chúng ta sử dụng hàm \(filter()\) như sau

dfilter(mytib, year == 2010) # chỉ lấy các quan sát có year là 2010
## # A tibble: 185 × 9
##    country          year infan…¹ life_…² ferti…³ popul…⁴      gdp conti…⁵ region
##    <fct>           <int>   <dbl>   <dbl>   <dbl>   <dbl>    <dbl> <fct>   <fct> 
##  1 Albania          2010    14.8    77.2    1.74  2.90e6  6.14e 9 Europe  South…
##  2 Algeria          2010    23.5    76      2.82  3.60e7  7.92e10 Africa  North…
##  3 Angola           2010   110.     57.6    6.22  2.12e7  2.61e10 Africa  Middl…
##  4 Antigua and Ba…  2010     7.7    75.8    2.13  8.72e4  8.37e 8 Americ… Carib…
##  5 Argentina        2010    13      75.8    2.22  4.12e7  4.34e11 Americ… South…
##  6 Armenia          2010    16.1    73      1.55  2.96e6  4.10e 9 Asia    Weste…
##  7 Aruba            2010    NA      75.1    1.7   1.02e5 NA       Americ… Carib…
##  8 Australia        2010     4.1    82      1.89  2.22e7  5.63e11 Oceania Austr…
##  9 Austria          2010     3.6    80.5    1.44  8.39e6  2.24e11 Europe  Weste…
## 10 Azerbaijan       2010    33.9    70.1    1.97  9.10e6  2.12e10 Asia    Weste…
## # … with 175 more rows, and abbreviated variable names ¹​infant_mortality,
## #   ²​life_expectancy, ³​fertility, ⁴​population, ⁵​continent

Sau khi chạy câu lệnh trên, R sẽ tạo thành một \(tibble()\) mới chỉ bao gồm các quan sát có giá trị cột \(year\) là 2010. Lưu ý rằng nếu bạn đọc muốn lưu lại giá trị sau mỗi lần thực hiện tính toán hãy gán giá trị trả lại vào một đối tượng mới. \(filter()\) có thể được thực hiện khi sử dụng nhiều cột, chẳng hạn như bạn đọc muốn lọc các quan sát của năm 2010 của các quốc gia Châu Âu

dfilter(mytib, year == 2010, continent == "Europe") 
## # A tibble: 39 × 9
##    country           year infan…¹ life_…² ferti…³ popul…⁴     gdp conti…⁵ region
##    <fct>            <int>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl> <fct>   <fct> 
##  1 Albania           2010    14.8    77.2    1.74  2.90e6 6.14e 9 Europe  South…
##  2 Austria           2010     3.6    80.5    1.44  8.39e6 2.24e11 Europe  Weste…
##  3 Belarus           2010     4.7    70.2    1.46  9.49e6 2.60e10 Europe  Easte…
##  4 Belgium           2010     3.6    80.1    1.84  1.09e7 2.67e11 Europe  Weste…
##  5 Bosnia and Herz…  2010     6.4    77.9    1.24  3.84e6 8.21e 9 Europe  South…
##  6 Bulgaria          2010    11.2    73.7    1.49  7.41e6 1.92e10 Europe  Easte…
##  7 Croatia           2010     4.6    76.7    1.47  4.32e6 2.80e10 Europe  South…
##  8 Czech Republic    2010     3.4    77.5    1.5   1.05e7 8.21e10 Europe  Easte…
##  9 Denmark           2010     3.3    79.4    1.88  5.55e6 1.69e11 Europe  North…
## 10 Estonia           2010     3.6    76.4    1.63  1.33e6 8.01e 9 Europe  North…
## # … with 29 more rows, and abbreviated variable names ¹​infant_mortality,
## #   ²​life_expectancy, ³​fertility, ⁴​population, ⁵​continent

6.4 Sắp xếp dữ liệu bằng hàm arrange()

Hàm arrange() sắp xếp các hàng của dữ liệu theo thứ tự tăng dần. Nguyên tắc sắp xếp cũng tương tự như hàm sort() khi làm trên véc-tơ, nghĩa là hàm bạn đọc có thể sắp xếp dữ liệu dựa theo bất kỳ kiểu dữ liệu nào. Chẳng hạn như để sắp xếp dữ liệu \(gapminder\) theo thứ tự tăng dần theo năm, theo Châu lục, và sau cùng là theo vùng, bạn đọc sử dụng câu lệnh như sau

arrange(mytib, year, continent, region) 
## # A tibble: 10,545 × 9
##    country     year infant_mort…¹ life_…² ferti…³ popul…⁴     gdp conti…⁵ region
##    <fct>      <int>         <dbl>   <dbl>   <dbl>   <dbl>   <dbl> <fct>   <fct> 
##  1 Burundi     1960         145.     40.6    6.95  2.79e6  3.41e8 Africa  Easte…
##  2 Comoros     1960         200      44.0    6.79  1.89e5 NA      Africa  Easte…
##  3 Djibouti    1960          NA      45.8    6.46  8.36e4 NA      Africa  Easte…
##  4 Eritrea     1960          NA      39.0    6.9   1.41e6 NA      Africa  Easte…
##  5 Ethiopia    1960         162      37.7    6.88  2.22e7 NA      Africa  Easte…
##  6 Kenya       1960         119.     47.4    7.95  8.11e6  2.12e9 Africa  Easte…
##  7 Madagascar  1960         112      42.0    7.3   5.10e6  2.09e9 Africa  Easte…
##  8 Malawi      1960         218.     38.5    6.91  3.62e6  3.48e8 Africa  Easte…
##  9 Mauritius   1960          67.8    58.7    6.17  6.60e5 NA      Africa  Easte…
## 10 Mozambique  1960         183      38.2    6.6   7.49e6 NA      Africa  Easte…
## # … with 10,535 more rows, and abbreviated variable names ¹​infant_mortality,
## #   ²​life_expectancy, ³​fertility, ⁴​population, ⁵​continent

Nếu muốn sắp xếp dữ liệu theo thứ tự giảm dần, nếu cột dữ liệu là kiểu số, bạn đọc chỉ cần thêm dấu “-” trước tên biến. Trong trường hợp cột dữ liệu kiểu bất kỳ, bạn đọc sử dụng hàm \(desc()\)

arrange(mytib, year, desc(continent), desc(region), -gdp) # tăng dần theo năm, giảm dần theo continent, region, gdp
## # A tibble: 10,545 × 9
##    country           year infan…¹ life_…² ferti…³ popul…⁴     gdp conti…⁵ region
##    <fct>            <int>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl> <fct>   <fct> 
##  1 French Polynesia  1960     NA     56.3    5.66   78083 NA      Oceania Polyn…
##  2 Samoa             1960     92     51.4    7.65  108645 NA      Oceania Polyn…
##  3 Tonga             1960     NA     61.2    7.36   61600 NA      Oceania Polyn…
##  4 Kiribati          1960     NA     45.8    6.95   41234 NA      Oceania Micro…
##  5 Micronesia, Fed…  1960     NA     56.8    6.93   44539 NA      Oceania Micro…
##  6 Papua New Guinea  1960    135.    38.6    6.28 1966957  8.37e8 Oceania Melan…
##  7 Fiji              1960     54     55.7    6.46  393383  4.37e8 Oceania Melan…
##  8 New Caledonia     1960     NA     56.4    5.22   78058 NA      Oceania Melan…
##  9 Solomon Islands   1960    132.    50.6    6.39  117869 NA      Oceania Melan…
## 10 Vanuatu           1960    107.    46.0    7.2    63701 NA      Oceania Melan…
## # … with 10,535 more rows, and abbreviated variable names ¹​infant_mortality,
## #   ²​life_expectancy, ³​fertility, ⁴​population, ⁵​continent

Lưu ý rằng nếu trong cột dữ liệu sử dụng để sắp xếp có giá trị \(NA\) thì các giá trị này luôn được sắp xếp xuống phía dưới của dữ liệu.

6.5 Kết hợp các hàm bằng toán tử pipe (\%>\%)

Trước khi giới thiệu các hàm khác dùng để biến đổi dữ liệu của thư viện \(dplyr\), chúng tôi muốn giới thiệu đến bạn đọc toán tử pipe (\(\%>\%\)). Đây là một công cụ vô cùng hữu hiệu khi bạn đọc thực hiện một chuỗi các phép biến đổi dữ liệu. Toán tử pipe được mượn từ toán học khi nói đến việc sử dụng các hàm số nối tiếp nhau. Pipe trong thư viện \(dplyr\) cũng có ý nghĩa tương tự khi bạn đọc sử dụng một chuỗi các hàm của thư viện này nhằm biến đổi dữ liệu. Ví dụ như khi bạn đọc muốn lấy ra từ dữ liệu \(gapminder\) ba quốc gia có thu nhập bình quân đầu người cao nhất trong năm 2000, bạn sẽ cần các phép biến đổi sau: - Thứ nhất: thêm cột thu nhập bình quân đầu người (sử dụng hàm \(mutate()\)) - Thứ hai: lọc dữ liệu theo năm, chỉ lấy dữ liệu của năm 2000. (sử dụng hàm \(filter()\)) - Thứ ba: lựa chọn cột tên quốc gia và cột thu nhập bình quân đầu người (sử dụng hàm \(select()\)) - Thứ tư: sắp xếp dữ liệu theo cột thu nhập bình quân đầu người, thứ tự sắp xếp là giảm dần (sử dụng hàm \(arrange()\)) - Thứ năm: lấy ra ba hàng đầu tiên của dữ liệu (sử dụng hàm \(head()\))

Nếu không sử dụng toán tử \(pipe\), sau mỗi bước ở trên bạn đọc sẽ phải lưu kết quả và gọi lại kết quả vào bước kế tiếp:

mytib1<-mutate(mytib,gdp_per_capita = gdp/population) # bước thứ nhất
mytib1<-dfilter(mytib1, year == 2010) # bước thứ hai
mytib1<-select(mytib1, country, gdp_per_capita) # bước thứ ba
mytib1<-arrange(mytib1, desc(gdp_per_capita)) # bước thứ tư
head(mytib1,3) # bước thứ năm
## # A tibble: 3 × 2
##   country    gdp_per_capita
##   <fct>               <dbl>
## 1 Luxembourg         52210.
## 2 Japan              40013.
## 3 Norway             39954.

Thay vì phải lưu lại dữ liệu sau mỗi lần sử dụng biến đổi dữ liệu và gọi lại kết quả để sử dụng cho bước tiếp theo, bạn đọc có thể sử dụng toán tử \(pipe\) như sau

mytib%>%mutate(gdp_per_capita = gdp/population)%>%
  dfilter(year == 2010)%>%
  select(country, gdp_per_capita)%>%
  arrange(desc(gdp_per_capita)) %>%
  head(3)
## # A tibble: 3 × 2
##   country    gdp_per_capita
##   <fct>               <dbl>
## 1 Luxembourg         52210.
## 2 Japan              40013.
## 3 Norway             39954.

Kết quả thu được hoàn toàn tương tự như trên tuy nhiên câu lệnh đã rõ ràng hơn rất nhiều. Từ phần này của cuốn sách, mọi phép biến đổi trên dữ liệu chúng tôi sẽ luôn luôn ưu tiên sử dụng toán tử \(pipe\) vì sự đơn giản và rõ ràng của các câu lệnh.

6.6 Tổng hợp dữ liệu bằng \(group\_by()\)\(summarise()\)

Hàm \(group\_by()\) là một công cụ hữu hiệu trong tổng hợp dữ liệu và tính toán theo nhóm. Chẳng hạn như từ dữ liệu \(gapminder\) bạn đọc muốn biết tổng thu nhập quốc dân (\(gdp\)) của một quốc gia là cao hay thấp so với tổng thu nhập quốc dân trung bình của châu lục (\(continent\)) trong năm tương ứng (\(year\)), bạn đọc cần phải thực hiện các thao tác sau: - Bước 1: Nhóm dữ liệu lại theo châu lục và theo năm - Bước 2: Tính tổng thu nhập quốc dân của châu lục trong năm đó, bỏ qua các nước không có quan sát - Bước 3: Đếm xem có bao nhiêu giá trị có quan sát - Bước 4: Lấy kết quả ở bước 2 chia cho kết quả của bước 3. - Bước 5: So sánh gdp của quốc gia với gdp trung bình của châu lục trong năm đó

Những bước như trên có thể được thực hiện một cách đơn giản thông qua hàm \(group\_by\) như sau

mytib%>%group_by(continent, year) %>%
  mutate(gdp_year_continent = mean(gdp,na.rm=TRUE))%>% # thêm cột gdp bình quân của châu lục theo năm
  ungroup()%>% # để dữ liệu trở lại trạng thái ban đầu (trước khi group)
  mutate(gdp_level = ifelse(gdp > gdp_year_continent, "High", "Low"))
## # A tibble: 10,545 × 11
##    country  year infan…¹ life_…² ferti…³ popul…⁴      gdp conti…⁵ region gdp_y…⁶
##    <fct>   <int>   <dbl>   <dbl>   <dbl>   <dbl>    <dbl> <fct>   <fct>    <dbl>
##  1 Albania  1960   115.     62.9    6.19  1.64e6 NA       Europe  South… 1.11e11
##  2 Algeria  1960   148.     47.5    7.65  1.11e7  1.38e10 Africa  North… 3.65e 9
##  3 Angola   1960   208      36.0    7.32  5.27e6 NA       Africa  Middl… 3.65e 9
##  4 Antigu…  1960    NA      63.0    4.43  5.47e4 NA       Americ… Carib… 1.15e11
##  5 Argent…  1960    59.9    65.4    3.11  2.06e7  1.08e11 Americ… South… 1.15e11
##  6 Armenia  1960    NA      66.9    4.55  1.87e6 NA       Asia    Weste… 6.12e10
##  7 Aruba    1960    NA      65.7    4.82  5.42e4 NA       Americ… Carib… 1.15e11
##  8 Austra…  1960    20.3    70.9    3.45  1.03e7  9.67e10 Oceania Austr… 3.27e10
##  9 Austria  1960    37.3    68.8    2.7   7.07e6  5.24e10 Europe  Weste… 1.11e11
## 10 Azerba…  1960    NA      61.3    5.57  3.90e6 NA       Asia    Weste… 6.12e10
## # … with 10,535 more rows, 1 more variable: gdp_level <chr>, and abbreviated
## #   variable names ¹​infant_mortality, ²​life_expectancy, ³​fertility,
## #   ⁴​population, ⁵​continent, ⁶​gdp_year_continent

Nếu đoạn câu lệnh trên không sử dụng câu lệnh \(group\_by\), hàm \(mean()\) trong câu lệnh \(mutate()\) sẽ thực hiên tính toán cho toàn bộ véc-tơ \(gdp\) của dữ liệu \(gapminder\). Nghĩa là cột mới được hình thành sẽ có giá trị giống nhau với tất cả các quan sát. Sau khi sử dụng hàm \(group\_by()\), mỗi khi chúng ta sử dụng một hàm tính toán trên một cột dữ liệu khác, cột dữ liệu đó sẽ được tính toán cho từng nhóm được định nghĩa bởi hàm \(group\_by\). Trong đoạn lệnh ở trên, hàm \(mean()\) sẽ tính giá trị trung bình của véc-tơ \(gdp\) cho từng châu lục theo từng năm. Thật vậy, chúng ta có thể kiểm tra giá trị ở hàng đầu tiên của cột \(gdp\_year\_continent\) (tương ứng với Albania - năm 1960 - châu Âu) sẽ là giá trị trung bình của véc-tơ \(gdp\) của các nước châu Âu trong năm 1960:

mytib%>%dfilter(year == 1960, continent == "Europe")%>%
  select(gdp)%>%mean()
## [1] NA

Hàm \(group\_by()\) kết hợp với \(summarise()\) sẽ tạo thành một dữ liệu mới mà mỗi hàng sẽ tương đương với một nhóm được quy định bởi hàm \(group\_by()\).

mytib%>%group_by(continent, year) %>%
  summarise(gdp_year_continent = mean(gdp,na.rm=TRUE))
## # A tibble: 285 × 3
## # Groups:   continent [5]
##    continent  year gdp_year_continent
##    <fct>     <int>              <dbl>
##  1 Africa     1960        3652247577.
##  2 Africa     1961        3642863976.
##  3 Africa     1962        3781167783.
##  4 Africa     1963        4107185952.
##  5 Africa     1964        4342684293.
##  6 Africa     1965        4618242478.
##  7 Africa     1966        4562215808.
##  8 Africa     1967        4592913225.
##  9 Africa     1968        4814235499.
## 10 Africa     1969        5157970620.
## # … with 275 more rows

Bạn đọc có thể thấy rằng dữ liệu mới (dưới dạng một \(tibble\)) được tạo thành, mỗi hàng là một châu lục trong một năm, với ba cột bao gồm hai cột được quy định bởi hàm \(group\_by()\)\(continent\), \(year\) và cột \(gdp\_year\_continent\) mới được tạo thành từ hàm \(summarise()\).

7 Trực quan hóa dữ liệu

p<-gapminder%>%filter(year<=2010)%>%
# AESTHETIC MAPPING
ggplot(aes(x=fertility,y=life_expectancy,size = population, fill= continent))+
# TAO DO THI SCATTERPLOT
geom_point(shape=21,alpha=0.6)+
# THAY DOI TITLE CUA DO THI, TRUC X, TRUC Y
labs(title = 'Năm: {as.integer(frame_time)}',
y = "Tuổi thọ trung bình",
x = "Tỷ lệ sinh trên mỗi phụ nữ")+
#GIOI HAN LAI GIA TRI TREN X,Y
xlim(0,10)+ylim(20,90)+
# SCALE LAI SIZE (POPULATION)
scale_size(range = c(1*2, 20*2)) +
# SCALE LAI MAU SAC THE0 DAI MAU "SET1" CUA BREWER
scale_color_brewer(palette = "Set1")+
# LAM TITLE THAY DOI THEO NAM
transition_time(year)+
#SIZE & FONT CHU
theme(,
plot.title = element_text(size = 20*2),
axis.title.x = element_text(size = 20*2),
axis.title.y = element_text(size = 20*2),
legend.text = element_text(size = 20*2,margin = margin(r = 30*2, unit = "pt")),
legend.title = element_text(size = 20*2),
#    legend.text=element_text(size=20*2),
)
#legend.key.size = element_rect(size = rel(1.5)),

# TAO DO THI DANG DONG
animate(p, renderer = gifski_renderer(),
width = 1600, #pixel chieu rong
height = 1600) # pixel chieu cao
## 
## Attaching package: 'dplyr'
## The following object is masked from 'package:gridExtra':
## 
##     combine
## The following object is masked from 'package:kableExtra':
## 
##     group_rows
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
## 
## Attaching package: 'lubridate'
## The following objects are masked from 'package:base':
## 
##     date, intersect, setdiff, union
## 
## Attaching package: 'plotly'
## The following object is masked from 'package:ggplot2':
## 
##     last_plot
## The following object is masked from 'package:stats':
## 
##     filter
## The following object is masked from 'package:graphics':
## 
##     layout

8 Trực quan hóa dữ liệu

Trực quan hóa dữ liệu là nghệ thuật mô tả dữ liệu thông qua việc sử dụng đồ họa và hình ảnh như các biểu đồ, sơ đồ, và cả hình ảnh động hoặc hình ảnh tương tác. Trực quan hóa dữ liệu là một phương pháp truyền đạt thông tin một cách trực quan và dễ hiểu từ người quản lý dữ liệu đến người tiếp nhận. Trực quan hóa giúp mô tả các mối quan hệ dữ liệu phức tạp, các thông tin chuyên sâu, và cả các vấn đề bất thường ẩn chứa trong dữ liệu.

Tại sao lại cần trực quan hóa dữ liệu ? Thứ nhất là do não bộ của con người sẽ cho phản ứng đối với hình ảnh, màu sắc, kích thước, khoảng cách, … tốt hơn nhiều so với các ký hiệu và con số. Thứ hai là do dữ liệu mà chúng ta phải đối mặt trong thời đại ngày nay ngày càng lớn và phức tạp. Trực quan hóa là phương pháp hiệu quả nhất để tìm ra các giá trị ẩn chứa bên trong dữ liệu. Đây chính là điểm khiến kỹ năng trực quan hóa dữ liệu được đánh giá là kỹ năng quan trọng nhất đối với những người phân tích dữ liệu.

Có nhiều công cụ để trực quan hóa dữ liệu. Tiêu biểu phải kể đến Power BI và Tableau. Đây là hai công cụ thân thiện với người dùng, cho phép người dùng tạo bảng điều khiển và báo cáo tương tác một cách nhanh chóng và dễ dàng. Cả hai đều có giao diện kiểu kéo và thả chuột giúp dễ dàng tạo hình ảnh trực quan mà không cần bất kỳ kỹ năng lập trình nào.

R sử dụng thư viện \(ggplot2\) để trực quan hóa dữ liệu. Sẽ là không dễ dàng cho người mới bắt đầu vẽ được đồ thị bằng \(ggplot2\). Điểm mạnh của \(ggplot2\) so với các công cụ như Power BI hay Tableau là cho phép người dùng tạo các hình ảnh có khả năng tùy biến cao. \(ggplot2\) là một lựa chọn phù hợp dành cho các nhà phân tích dữ liệu, những người cảm thấy hứng thú với việc viết các câu lệnh để tạo ra các hình ảnh trực quan phức tạp, và đúng theo ý muốn của mình. Với một chút kinh nghiệm về Power BI và Tableau, cùng với nhiều hơn một chút kinh nghiệm về \(ggplot2\), chúng tôi cho rằng bạn đọc nên làm quen với cả hai cách trực quan hóa dữ liệu. Khi bạn phải tạo các báo cáo trực quan trong một thời gian ngắn, Power BI hay Tableau sẽ là lựa chọn tối ưu. Khi bạn muốn vẽ những hình ảnh phức tạp, có tính cá nhân cao, và bạn có thời gian để làm việc đó, hãy sử dụng \(ggplot2\).

8.1 Giới thiệu về \(ggplot2\)

\(ggplot2\) là một thư viện để trực quan hóa dữ liệu trong R. Ngoài \(ggplot2\), bạn đọc cũng có thể sử dụng các đồ thị cơ bản của R, hoặc sử dụng các thư viện khác như \(lattice\) để vẽ đồ thị. Tuy nhiên, không giống như hầu hết các công cụ khác, \(ggplot2\) trực quan hóa dữ liệu dựa trên Ngữ pháp của đồ thị (Wilkinson 2005). Hai chữ \(gg\) bắt đầu có nghĩa là Grammar of Graphic hay Ngữ pháp của đồ thị. Ngữ pháp cho phép bạn đọc vẽ đồ thị bằng cách kết hợp các cấu phần độc lập lại với nhau. Đây chính là điểm mạnh của \(ggplot2\). Thay vì bị giới hạn ở các bộ đồ thị đã được xác định trước, bạn đọc có thể tạo đồ thị mới phù hợp với mục tiêu của mình. Ý tưởng phải học ngữ pháp để vẽ đồ thị có thể làm cho bạn đọc cảm thấy nản chí, nhưng sự thật là ngữ pháp của \(ggplot2\) thực sự dễ học. Chỉ có một số nguyên tắc cốt lõi đơn giản và có rất ít trường hợp đặc biệt. Khi đã thông thạo Ngữ pháp của đồ thị, ngoài tạo ra những đồ thị quen thuộc, bạn đọc còn có thể tạo ra những đồ thị mới hơn, đẹp hơn và mang tính cách riêng. Bạn đọc có thể gặp khó khăn một chút thời gian ban đầu nhưng chúng tôi tin rằng khi đã quen với \(ggplot2\) thì sẽ rất ít bạn đọc muốn quay lại với các công cụ trực quan hóa dữ liệu khác.

Hãy thử xem một chút \(ggplot2\) trực quan hóa dữ liệu như thế nào. Chúng ta sẽ bắt đầu với một dữ liệu có tên là \(murders\) trong thư viện \(dslabs\). Giả sử bạn muốn du lịch đến Mỹ nhưng bạn lo ngại về việc cho phép sử dụng súng ở quốc gia này và bạn muốn biết ở những bang nào có tỷ lệ số vụ sát nhân bằng súng cao. Dữ liệu \(murders\) là dữ liệu do FBI cung cấp về số vụ sát nhân bằng súng tại các bang của nước Mỹ vào năm 2010. Do bạn đọc đã biết về \(data.frame()\), nên bạn có thể tìm hiểu về dữ liệu bằng các hàm như head(), str(), view()

library(dslabs)
head(murders)
##        state abb region population total
## 1    Alabama  AL  South    4779736   135
## 2     Alaska  AK   West     710231    19
## 3    Arizona  AZ   West    6392017   232
## 4   Arkansas  AR  South    2915918    93
## 5 California  CA   West   37253956  1257
## 6   Colorado  CO   West    5029196    65

Thật khó để có thể có được cái nhìn tổng thể về dữ liệu nếu chỉ nhìn vào các bảng, các con số, ký hiệu như trên. Thay vì sử dụng con số, bạn đọc có thể trình bày dữ liệu \(murders\) dưới dạng một đồ thị rải điểm (scatter plot) như sau

Chúng tôi đã sử dụng một vài kỹ thuật biến đổi dữ liệu để vẽ đồ thị ở trên:

  • Do các biến total (tổng số vụ sát nhân) và biến population (dân số của mỗi bang) đều có đuôi dài, nghĩa là có nhiều điểm tập trung ở khu vực trung tâm, và một số ít điểm tập trung ở phía đuôi bên phải, do đó thay vì sử dụng chính xác giá trị của các biến trên đồ thị, các điểm của đồ thị rải điểm sẽ phân bố không đồng đều. Giá trị hiển thị trên đồ thị đã được điều chính lại theo hàm \(log()\) cơ số 10.

  • Chúng tôi thêm vào một đường thẳng tuyến tính (đường kẻ màu xám đi qua trung tâm) để mô tả mối quan hệ chung giữa hai biến \(total\)\(population\).

Dựa trên đồ thị rải điểm ở trên, bạn đọc có thể đưa ra được ngay các nhận xét như sau

  • Bang nào có dân số càng cao thì số vụ sát nhân bằng súng càng nhiều.

  • Hầu hết các bang nằm phía trên đường trung bình là các bang ở miền Nam (màu đỏ).

  • Các vùng còn lại không có sự phân biệt rõ ràng.

  • Bang “District of Columbia” là bang nằm cao hơn hẳn so với đường trung bình, và cũng là bang có tỷ lệ số vụ sát nhân bằng súng cao nhất.

  • Bang California có tổng số vụ sát nhân bằng súng lớn nhất, nhưng tỷ lệ số vụ sát nhân bằng súng trên đầu người chỉ bằng mức trung bình chung.

Không dễ dàng để đưa ra được các nhận xét như trên nếu chỉ dựa trên quan sát con số và dữ liệu. Thay vì biểu diễn dưới dạng con số, chúng ta có thể đưa ra nhiều phân tích có ý nghĩa về dữ liệu khi sử dụng đồ thị như trên.

Wilkinson (2005) giới thiệu khái niệm Ngữ pháp đồ thị để mô tả các thành phần cơ bản làm nền tảng cho tất cả các đồ thị sử dụng và cách các thành phần tương tác trong mô tả dữ liệu. Ngữ pháp đồ thị là mô tả chính xác nhất cho câu hỏi đồ thị trực quan hóa dữ liệu là gì? Thư viện \(ggplot2\) được Wickham giới thiệu vào 2009 xây dựng dựa trên ngữ pháp đồ thị mà Wilkinson đã đề cập bằng cách tập trung vào việc xây dựng đồ thị dựa trên nhiều lớp (layer). Nhìn chung, ngữ pháp đồ thị cho chúng ta biết quy tắc cho tương ứng các biến của dữ liệu với các thuộc tính thẩm mỹ (các aesthetic attributions) của đối tượng hình ảnh xuất hiện (các geometries). Đồ thị trong \(ggplot2\) cũng có thể bao gồm các mô hình thống kê của dữ liệu và hệ tọa độ mà đồ thị sử dụng. Bạn đọc cũng có thể chia dữ liệu thành các tập hợp con dựa trên các biến rời rạc và mô tả dữ liệu thông qua một nhóm các đồ thị con thông qua kỹ thuật facetting. Sự kết hợp của các thành phần độc lập kể trên tạo nên một đồ thị mô tả dữ liệu.

Bạn đọc không cần phải lo lắng nếu khái niệm Ngữ pháp đồ thị ở trên không có ý nghĩa ngay lập tức. Trong phần sau của cuốn sách, chúng tôi sẽ nói về ngữ pháp đồ thị một cách chi tiết hơn. Bạn sẽ có nhiều cơ hội hơn để tìm hiểu về Ngữ pháp và các sử dụng ngữ pháp để các cấu phần độc lập của một đồ thị hoạt động cùng nhau. Trong phần giới thiệu này, chúng tôi muốn bạn đọc hãy ghi nhớ thành phần độc lập tạo nên một đồ thị cơ bản bao gồm có

  1. Dữ liệu (Data) là dữ liệu hay tập hợp các dữ liệu mà bạn đọc muốn trực quan hóa. Thông thường thì chỉ có một dữ liệu chính mà bạn đọc muốn minh họa cho người tiếp nhận dữ liệu, trong khi các dữ liệu khác được sử dụng với mục đích để mô tả dữ liệu chính. Một ví dụ điển hình của dữ liệu phụ là dữ liệu kiểu bản đồ. Chẳng hạn như khi bạn đọc muốn mô tả về dữ liệu \(murder\), bạn đọc có thể sử dụng dữ liệu về bản đồ nước Mỹ để mô tả tốt hơn về dữ liệu \(murder\).

  2. Hình dạng đồ họa (các \(geometries\) hay viết tắt là các \(geom\)) là những hình dạng đồ họa mà chúng ta muốn nhìn thấy trên đồ thị. Các hình dạng này có thể là các điểm, các thanh, các đường.

  3. Các ánh xạ thẩm mỹ (Aesthetic mapping) là các quy tắc cho tương ứng từ các biến (cột của dữ liệu) đến các thuộc tính thẩm mỹ (aesthetic attribution) của các hình dạng đồ họa. Các thuộc tính thẩm mỹ có thể là hình dạng, màu sắc, độ đậm nhạt, …

  4. Các mô hình hay biến đổi thống kê (statistics hay viết tắt là stats) là các quy tắc tóm tắt dữ liệu, các mô hình xây dựng trên dữ liệu được mô tả dưới dạng một hình dạng đồ họa nhằm tăng tính dễ hiểu cho đồ thị hoặc làm nổi bật một xu thế nào đó. Ví dụ như trong đồ thị rải điểm mô tả dữ liệu \(murder\), chúng tôi đã sử dụng một mô hình tuyến tính mô tả mối quan hệ giữa biến \(total\) và biến \(population\) với mục đích phân loại ra các bang có tỷ lệ số vụ sát nhân bằng súng thấp hơn và các bang có tỷ lệ số vụ sát nhân bằng súng cao hơn so với chung bình chung.

  5. Hệ tọa độ (Cordinate) mô tả cách dữ liệu được trực quan hóa trên mặt phẳng của đồ họa. Đa số các trường hợp chúng ta sẽ sử dụng hệ tọa độ Descartes, nhưng cũng có một số hệ tọa độ khác có thể sử dụng bao gồm tọa độ cực và bản đồ.

  6. Một thành phần mô tả cách dữ liệu được hiển thị là chia nhỏ dữ liệu để mô tả bằng một nhóm các đồ thị thay vì một đồ thị duy nhất được gọi là facetting. Thành phần này thường được sử dụng để mô tả dữ liệu có kích thước lớn và hoặc chúng ta muốn so sánh trực quan dữ liệu ở các nhóm khác nhau.

  7. Thành phần cuối cùng của đồ thị là ngữ cảnh của đồ thị hay các themes. Theme quy định khung hoặc nền mà đồ thị được hiển thị chẳng hạn như kích thước phông chữ hoặc màu nền. Mặc dù các giá trị mặc định trong \(ggplot2\) đã được lựa chọn hợp lý nhưng bạn đọc cũng có thể cần tham khảo các tài liệu tham khảo khác để tạo ra một ngữ cảnh phù hợp hơn cho đồ thị của mình.

Mỗi khi vẽ một đồ thị \(ggplot\), bạn đọc cần tự định nghĩa ít nhất ba thành phần: 1. Dữ liệu; 2. Các hình dạng đồ họa; và 3. Các ánh xạ thẩm mỹ. Các thành phần 5. Hệ tọa độ; và 7. Ngữ cảnh; sẽ được tự động gán cho các giá trị mặc định nếu bạn đọc không quy định trong câu lệnh. Và các thành phần 4. Mô hình; và 6. Facetting; chỉ xuất hiện khi bạn đọc gọi lên trong câu lệnh của mình.

Trước khi đi vào giới thiệu chi tiết các cách tạo nên một đồ thị trực quan hóa, bạn đọc cũng cần biết được các hạn chế khi trực quan hóa dữ liệu bằng \(ggplot2\):

  • \(ggplot2\) là một thư viện của R nên bạn đọc cần có kỹ năng viết câu lệnh R tương đối thành thạo.

  • \(ggplot2\) không gợi ý bạn đọc nên sử dụng đồ thị nào khi gặp một dữ liệu cụ thể. Điều đó cũng có nghĩa là bạn đọc cần có một chút kinh nghiệm về trực quan hóa dữ liệu trước khi sử dụng \(ggplot2\).

  • \(ggplot2\) không được phát triển để vẽ các đồ thị động hay đồ thị tương tác mà chỉ tập trung vào vẽ các đồ thị tĩnh. Muốn vẽ các đồ thị tương tác hay đồ thị động trong \(ggplot2\) bạn đọc phải sử dụng các packages đi kèm như \(gganimate\) hay \(ggplotly\).

Để kết thúc phần giới thiệu về \(ggplot2\), chúng tôi sẽ sử dụng \(ggplot2\) kết hợp với \(gganimate\) để kể một câu chuyện (story telling) về sự phát triển của các quốc gia trên thế giới từ năm 1960 đến năm 2010 thông qua hai khía cạnh là tuổi thọ trung bình và thu nhập bình quân đầu người. Dữ liệu chính được sử dụng là dữ liệu \(gapminder\).

8.2 Tạo một đồ thị \(ggplot2\) cơ bản

Trước khi giới thiệu chi tiết về các thành phần độc lập của đồ thị và cách sử dụng ngữ pháp của đồ thị, chúng tôi nghĩ rằng sẽ tốt hơn nếu bạn đọc bắt đầu vẽ các đồ thị đơn giản bằng cách copy và dán các câu lệnh vẽ đồ thị trước. Sau khi thực thi một vài lần, bạn đọc sẽ có “cảm nhận” được phần nào cách mà một đồ thị của \(ggplot2\) được xây dựng. Dữ liệu chúng tôi sử dụng để trực quan hóa trong suốt chương sách này là dữ liệu \(gapminder\), dữ liệu về sức khỏe và thu nhập của tất cả các quốc gia trên thế giới bắt đầu từ năm 1960 đến năm 2016. Bạn đọc hãy đảm bảo rằng mình đã đọc và hiểu một cách cơ bản về dữ liệu này.

Bạn đọc có thể thấy rằng dữ liệu \(gapminder\) có nhiều giá trị không quan sát được trong năm 2016. Hai cột có tỷ lệ không quan sát được qua các năm lớn là \(infant\_mortality\)\(gdp\). Riêng biến \(gdp\) là gần như không quan sát được từ năm 2012 đến 2016. Do chỉ sử dụng dữ liệu với mục đích trực quan hóa nên chúng tôi sẽ tiền xử lý dữ liệu một cách đơn giản là xóa các quan sát của các năm 2012 đến 2016. Giá trị không quan sát được từ năm 1960 đến 2012 sẽ được thay thế bằng mô hình rừng ngẫu nhiên. Dữ liệu sau bước tiền xử lý này được gọi là \(gapminder\_1\)

Hàm số để vẽ đồ thị của thư viện \(ggplot2\) là hàm ggplot(). Bạn đọc hãy nhớ rằng ba thành phần bắt buộc phải có của một đồ thị là 1. Dữ liệu; 2. (Ít nhất) Một hình dạng đồ họa; và 3. Ánh xạ thẩm mỹ. Đồ thị dưới đây mô tả hai biến gdp bình quân đầu người và tuổi thọ trung bình của các quốc gia trên thế giới vào năm 2011. Bạn đọc có thể copy các đoạn lệnh ở dưới vào cửa sổ R script và thực hiện giống như các câu lệnh thông thường.

dat<-gapminder%>%filter(year==2011)%>%mutate(gdp_per_capita = gdp/population)
ggplot(dat, aes(x = life_expectancy, y = gdp_per_capita)) + geom_point()

Trong câu lệnh ggplot() ở trên, dữ liệu được đưa vào là \(data.frame\) có tên là \(dat\), hình dạng đồ họa là các điểm trên trục tọa độ Descartes. Hình dạng đồ họa này được gọi bằng hàm geom_point(). Ánh xạ thẩm mỹ được gọi thông qua hàm aes() nằm trong hàm ggplot(). Trong ánh xạ thẩm mỹ ở trên, chúng ta đã cho tương ứng biến \(life\_expectancy\) với giá trị trên trục \(x\) của trục tọa độ Descartes, biến \(gdp\_per\_capita\) với giá trị trên trục \(y\) của trục tọa độ Descartes.

Bạn đọc đã có thể thấy được một vài thông tin phán ánh trên dữ liệu.

  • Có mối liên hệ đồng biến giữa tuổi thọ trung bình và thu nhập bình quân đầu người. Quốc gia nào có thu nhập bình quân đầu người cao thì tuổi thọ trung bình cũng sẽ cao. Điều này khá hợp lý bởi các quốc gia có thu nhập trung bình cao thường là các nước phát triển có hệ thống chăm sóc sức khỏe tốt, do đó tuổi thọ trung bình cũng sẽ cao.

  • Mối liên hệ đồng biến nhưng không tuyến tính, thu nhập bình quân đầu người tăng nhanh hơn rất nhiều ro với tuổi thọ trung bình.

  • Có một vài điểm có khả năng là ngoại lai trong mối liên hệ tuyến tính này. Đây là các quốc gia có mức thu nhập bình quân khá cao (từ 10 nghìn USD - 20 nghìn USD/1 người) nhưng lại có tuổi thọ trung bình không cao. Tuy nhiên chỉ với các thông tin như trên chúng ta không thể đưa ra giải thích cho các giá trị có khả năng cao là ngoại lai này.

Hình dạng đồ họa là những gì mà bạn đọc nhìn thấy trên đồ thị của mình. Khi gọi các hình dạng đồ họa thư viện \(ggplot2\) luôn luôn sử dụng các hàm số bắt đầu bởi geom là viết tắt của \(geometries\). Bạn đọc có thể thử với một vài hình dạng đồ họa quen thuộc như dưới đây

## geom_histogram() sử dụng các thanh để mô tả phân phối của một biến liên tục
ggplot(dat,aes(x = gdp_per_capita))+geom_histogram()

## geom_bar() sử dụng các thanh để mô tả phân phối của một biến rời rạc
ggplot(dat,aes(x = continent))+geom_bar()

## geom_boxplot() sử dụng các hình hộp để mô tả phân phối của biến liên tục
ggplot(dat,aes(x = continent, y = life_expectancy))+geom_boxplot()

## geom_line() sử dụng đường nối các điểm theo thứ tự điểm xuất hiện
dat1<-filter(gapminder, year<=2011, country == "United States")%>%select(year,gdp)
ggplot(dat1,aes(x = year, y = gdp))+geom_line()

Còn rất nhiều các hàm \(geom_*()\) khác có thể được sử dụng để tạo đồ thị trong thư viện \(ggplot2\). Bạn đọc có thể xem danh sách các \(geom\) thường sử dụng trong danh sách đính kèm dưới đây.

https://www.maths.usyd.edu.au/u/UG/SM/STAT3022/r/current/Misc/data-visualization-2.1.pdf

Bạn đọc có thể thầy rằng CHEAT SHEET cũng đã có gợi ý cho người sử dụng nên dùng hàm geom_*() nào trong từng trường hợp. Chẳng hạn như geom_point() được khuyên dùng trong trường hợp mô tả hai biến liên tục. Đồng thời, mỗi hàm geom_*() sẽ có một danh sách các thuộc tính thẩm mỹ đi kèm. Đối với geom_point() các thuộc tính thẩm mỹ bao gồm \(x\), \(y\), \(alpha\), \(color\), \(fill\), \(shape\), \(size\), và \(stroke\). Bạn đọc hướng dẫn sử dụng của hàm geom_point() để biết các thuộc tính thẩm mỹ này có ý nghĩa như thế nào. Trong các thuộc tính thẩm mỹ này, \(color\), \(fill\), \(shape\)\(size\) là các thuộc tính thẩm mỹ xuất hiện ở nhiều hàm geom_*() khác. Đây là các thuộc tính thẩm mỹ thường xuyên được sử dụng để tăng khả năng mô tả dữ liệu của các đồ thị.

Để mô tả tốt hơn mối quan hệ giữa thu nhập bình quân đầu người và tuổi thọ trung bình của các quốc gia trên thế giới, chúng ta cần thêm thông tin vào đồ thị ở trên. Một phương pháp đơn giản để thêm biến khác vào một đồ thị là ánh xạ biến đó đến một trong các thuộc tính thẩm mỹ của đồ thị được vẽ bởi hàm geom_point(). Biến được thêm vào dưới đây là biến \(continent\). Chúng ta sẽ ánh xạ biến đó tương ứng với thuộc tính thẩm mỹ \(color\) như sau

dat<-gapminder%>%filter(year==2011)%>%mutate(gdp_per_capita = gdp/population)
ggplot(dat, aes(x = life_expectancy, y = gdp_per_capita, color = continent)) +
  geom_point()

Chúng ta đã có thể đưa ra thêm các phân tích về mối liên hệ giữa tuổi thọ trung bình và thu nhập bình quân. Có sự phân bố không đồng đều về thu nhập bình quân và tuổi thọ trung bình của các quốc gia trên thế giới, đa số các quốc gia Châu Phi (màu đỏ) có thu nhập bình quân đầu người thấp và tuổi thọ trung bình thấp; các quốc gia Châu Âu (màu xanh da trời) có thu nhập bình quân đầu người cao và tuổi thọ trung bình cao. Có sự phân hóa rõ ràng ở Châu Đại Dương và Châu Mỹ, một vài quốc gia nằm trong nhóm các nước có thu nhập cao, tuổi thọ trung bình cao trong khi đa số các quốc gia còn lại nằm trong nhóm thu nhập thấp và tuổi thọ trung bình thấp. Sự phân hóa ở Châu Á không quá rõ ràng.

Có một nguyên tắc là thuộc tính thẩm mỹ \(color\) thường được sử dụng với biến rời rạc và thuộc tính thẩm mỹ \(size\) thường được sử dụng với biến liên tục. Thuộc tính thẩm mỹ \(shape\) chỉ có thể được sử dụng với biến rời rạc, R sẽ báo lỗi nếu bạn ánh xạ một biến liên tục vào \(shape\). Có 21 giá trị khác nhau trong dành cho thuộc tính thẩm mỹ \(shape\) do đó R sẽ có cảnh báo nếu bạn đọc ánh xạ một biến rời rạc có nhiều hơn 21 giá trị.

Đồ thị dưới đây thêm biến \(population\) vào đồ thị bằng cách sử dụng thuộc tính thẩm mỹ \(size\). Bạn đọc hãy luôn nhớ rằng để khai báo ánh xạ thẩm mỹ từ một biến đến một thuộc tính thẩm mỹ, hãy luôn luôn khai báo bên trong hàm aes().

ggplot(dat, aes(x = life_expectancy, y = gdp_per_capita, 
                color = continent, size = population)) + geom_point(alpha = 0.4)

Tham số \(alpha\) sử dụng trong hàm geom_point() trong trường hợp dữ liệu có nhiều điểm bị trùng lên nhau. Chúng ta sẽ thảo luận kỹ hơn về thuộc tính thẩm mỹ này trong phần sau của chương. Khi thêm biến \(population\) vào có thể làm đồ thị có thêm thông tin, chẳng hạn như bạn đọc có thể nhận ra vị trí của các quốc gia đông dân tiêu biểu như Trung Quốc và Ấn Độ vào năm 2011 vẫn nằm trong nhóm các nước có thu nhập bình quân đầu người thấp; hoặc cũng có thể nhận ra Mỹ và Nhật Bản là các quốc gia nằm ở góc trên bên phải là các nước cũng có dân số tương đối lớn. Tuy nhiên, bạn đọc cũng có thể nhận ra rằng khi cùng sử dụng nhiều thuộc tính thẩm mỹ trên một đồ thị, hiệu quả sẽ không được như mong muốn.

Với các dữ liệu có nhiều quan sát, bạn đọc có thể chia nhỏ dữ liệu thành các nhóm và tạo đồ thị cho từng nhóm.

ggplot(dat, aes(x = life_expectancy, y = gdp_per_capita, size = population)) + 
  geom_point(alpha = 0.4)+
  facet_wrap(~continent)

Sử dụng năm đồ thị có cùng trục tọa độ \(x\)\(y\) để mô tả mối quan hệ giữa thu nhập bình quân đầu người và tuổi thọ trung bình là rõ ràng hơn rất nhiều so với sử dụng một đồ thị duy nhất và phân biệt bằng màu sắc.

Thành phần cuối cùng mà bạn đọc có thể thêm vào đồ thị \(ggplot2\) là ngữ cảnh, hay các \(themes\). Có một số \(theme\) có sẵn hoặc có các \(theme\) nằm trong các thư viện cài đặt bổ sung. Chúng ta sẽ nói chi tiết về cách tùy chỉnh \(theme\) hoặc tự tạo \(theme\) ở phần sau của chương. Trong đoạn câu lệnh dưới đây, chúng tôi sử dụng theme_minimal() là một \(theme\) có sẵn trong thư viện \(ggplot2\).

ggplot(dat, aes(x = life_expectancy, y = gdp_per_capita, size = population)) + 
  geom_point(shape = 21, alpha = 0.7, fill = "lightskyblue")+
  facet_wrap(~continent)+
  # thêm title
  labs( title = "Thu nhập bình quân và tuổi thọ trung bình",
        subtitle = "Các quốc gia trên thế giới năm 2011")+
  xlab("Tuổi thọ trung bình (năm)")+
  ylab("Gdp bình quân đầu người (USD)")+
  theme_minimal()# thêm ngữ cảnh

Còn một thành phần khác chưa được nhắc đến khi tạo đồ thị là các \(statistics\) hay viết tắt là các \(stats\). Tuy nhiên đây là thành phần phức tạp nhất và liên quan đến các kiến thức về xây dựng mô hình trên dữ liệu nên chúng tôi không sử dụng trong phần này. Mục tiêu của chúng tôi là để bạn đọc làm quen và tự tạo các đồ thị đơn giản bằng các dòng lệnh của thư viện \(ggplot2\).

8.3 Cấu trúc nhiều lớp và ngữ pháp của đồ thị \(ggplot2\).

Cấu trúc theo lớp (layer) của đồ thị \(ggplot2\) giúp cho người phân tích dữ liệu có thể xây dựng đồ thị của mình theo hướng có cấu trúc. Đồ thị trong \(ggplot2\) từ đơn giản đến phức tạp đều được tạo thành từ một hoặc nhiều lớp. Mỗi lớp trong đồ thị có mục tiêu hiển thị khác nhau:

  • Mục tiêu thứ nhất và cũng là mục tiêu chính, đó là để hiển thị dữ liệu. Luôn luôn có một hoặc một vài lớp (chính) với mục tiêu mô tả dữ liệu thô, mô tả cấu trúc tổng thể và các giá trị ngoại lai của dữ liệu. Lớp này xuất hiện trên tất cả các đồ thị. Trong giai đoạn đầu của quá trình khai phá dữ liệu bằng trực quan hóa, lớp này thường xuất hiện duy nhất. Đơn giản như khi mô tả mỗi quan hệ giữa gdp bình quân đầu người và tuổi thọ trung bình của tất cả các quốc gia trên thế giới, lớp đồ thị được hiển thị bằng hàm geom_point() là lớp hiển thị dữ liệu.

  • Các lớp có mục tiêu tóm tắt và mô tả ý nghĩa thống kê của dữ liệu. Bằng cách thêm vào đồ thị các mô hình, hoặc hiển thị các dự đoán dựa trên mô hình người tiếp nhận dữ liệu, hoặc bản thân người phân tích dữ liệu sẽ nhận biết được những giá trị bên trong dữ liệu, hoặc những chi tiết mà khi xây dựng mô hình có thể bỏ sót.

  • Các lớp có mục tiêu thêm vào ngữ cảnh của dữ liệu. Các lớp này hiển thị bối cảnh nền, thêm vào các chú thích giúp mang lại ý nghĩa cho dữ liệu thô hoặc các giá trị tham chiếu nhằm hỗ trợ việc so sánh hoặc đánh giá. Đây thường là lớp cuối cùng được thêm vào trong đồ thị.

Lớp chính của đồ thị có thể bao gồm bảy thành phần độc lập giống như chúng ta đã giới thiệu ở phần đầu. Cấu trúc của các lớp còn lại của đồ thị \(ggplot2\) có thể bao gồm các thành phần sau

  1. Dữ liệu: nếu bạn không khai báo dữ liệu trong mỗi lớp, \(ggplot2\) sẽ sử dụng dữ liệu ban đầu là giá trị mặc định

  2. Ánh xạ thẩm mỹ: được khai báo trong hàm aes() trong mỗi lớp, nếu không có khai báo \(ggplot2\) sẽ tìm ánh xạ thẩm mỹ trong hàm ggplot(). Trong trường hợp không tìm thấy ánh xạ thẩm mỹ nào được khai báo, \(ggplot2\) sử dụng giá trị mặc định.

  3. Một hình dạng đồ họa: được gọi từ các hàm geom_*()

  4. Một biến đổi thống kê hoặc một tóm tắt cơ bản của dữ liệu được gọi từ hàm \(stat_*()\)

  5. Vị trí xuất hiện của lớp đó. Chúng ta sẽ thảo luận vấn đề này chi tiết trong phần sau của chương.

Khi đồ thị chỉ có một lớp chính với mục tiêu hiển thị dữ liệu thô, bạn không cần phải am hiểu về ngữ pháp của đồ thị. Bạn đọc chỉ cần khai báo chính xác ánh xạ thẩm mỹ trong hàm aes() để có được kết quả mong muốn. Tuy nhiên khi xây dựng đồ thị có nhiều lớp, bạn đọc cần phải nắm được ngữ pháp để kết hợp các lớp lại với nhau theo ý muốn của bạn.

8.3.1 Ánh xạ thẩm mỹ trong đồ thị có nhiều lớp.

Hãy quan sát ví dụ ở dưới đây, khi bạn muốn thêm vào một đường cong mô tả xu thế mối quan hệ giữa gdp bình quân đầu người và tuổi thọ trung bình của các quốc gia trên thế giới vào năm 2011.

## Hình bên trái
p1<-dat%>%ggplot(aes(x = life_expectancy, y = gdp_per_capita, 
                color = continent)) + 
  geom_point(alpha = 0.4)+
  geom_smooth(se=FALSE)

## Hình bên phải
p2<-dat%>%ggplot(aes(x = life_expectancy, y = gdp_per_capita))+
  geom_point(aes(color = continent), alpha = 0.4) + 
  geom_smooth(se=FALSE)

## Vẽ p1 và p2 trên cùng một đồ thị
grid.arrange(p1,p2,nrow= 1 , ncol = 2)

Bạn đọc có thể thấy sự khác nhau giữa hai đồ thị là các đường mô tả mối quan hệ giữa tuổi thọ bình quân và gdp bình quân đầu người được xây dựng theo từng lục địa (hình bên trái) và được xây dựng cho tất cả các quốc gia (hình bên phải). Sự khác biệt là do trong hình bên phải, thay vì khai báo ánh xạ thẩm mỹ từ biến \(continent\) tới thuộc tính thẩm mỹ \(color\) trong hàm ggplot(), chúng tôi đã khai báo ánh xạ thẩm mỹ này trog hàm geom_point().

Nếu như hàm geom_point() là lớp chính mô tả dữ liệu thô thì hàm geom_smooth() là lớp phụ được thêm vào nhằm tăng khả năng mô tả của dữ liệu. Các ánh xạ thẩm mỹ được khai báo trong hàm ggplot() cũng giống như các biến toàn cục trong một đồ thị cụ thể, trong khi các ánh xạ thẩm mỹ được khai báo trong các hàm geom_*() giống như khai báo giá trị cho các biến cục bộ trong môi trường của hàm số đó. Các biến cục bộ nếu không được khai báo trong các hàm geom_*() sẽ được tìm trên môi trường toàn cục của hàm ggplot(). Trong trường hợp trong các hàm geom_*()ggplot() đều không được khai báo giá trị, biến sẽ nhận giá trị mặc định.

Hãy quay trở lại đoạn câu lệnh ở trên. Trong hình bên trái, các thuộc tính thẩm mỹ \(x\), \(y\), và \(color\) được khai báo trong hàm ggplot(); đồng thời trong các hàm geom_point()geom_smooth() không khai báo các ánh xạ thẩm mỹ; do đó cả hai hàm này đều hiểu các thuộc tính thẩm mỹ \(x\), \(y\), và \(color\) giống như khai báo ban đầu. Trong hình bên phải, hai thuộc tính thẩm mỹ \(x\)\(y\) được khai báo trong hàm ggplot() trong khi thuộc tính thẩm mỹ \(color\) được khai báo bên trong hàm geom_point(). Do đó, hàm geom_smooth() chỉ hiểu hai thuộc tính thẩm mỹ \(x\)\(y\) như được khai báo trong ggplot().

geom_point() geom_smooth()
Hình bên trái x, y và color x, y và color
Hình bên phải x, y, và color x, y

Hàm geom_smooth() khi thuộc tính thẩm mỹ \(color\) nhận giá trị mặc định sẽ xây dựng một mô hình được gọi là locally estimated scatterplot smoothing hay loess nhằm mô tả mỗi quan hệ giữa biến liên tục \(y\) theo một biến liên tục \(x\). Nếu thuộc tính thẩm mỹ \(color\) được khai báo giá trị tương ứng với một biến rời rạc, geom_smooth() sẽ chia dữ liệu thành các nhóm tương ứng với \(color\) trước khi xây dựng mô hình mà \(y\) phụ thuộc vào \(x\). Điều này giải thích tại sao trong hình bên trái có 5 mô hình được xây dựng tương ứng với năm lục địa, trong khi trong hình bên phải chỉ có một mô hình duy nhất được xây dựng cho tất cả các quốc gia trên thế giới.

Vậy khi nào bạn nên khai báo ánh xạ thẩm mỹ trong hàm ggplot() và khi nào bạn nên khai báo ánh xạ thẩm mỹ bên trong hàm geom_*()? Câu trả lời là nếu đa số các lớp bạn đọc sử dụng chung một dữ liệu và chung các ánh xạ thẩm mỹ, bạn nên khai báo ánh xạ thẩm mỹ bên trong hàm ggplot(). Còn trong trường hợp đa số các lớp sử dụng sữ liệu khác nhau, hoặc ánh xạ thẩm mỹ khác nhau, bạn hãy khai báo ánh xạ thẩm mỹ bên trong mỗi hàm geom_*(). Trong trường hợp bạn dùng một hàm geom_()* và không muốn sử dụng ánh xạ thẩm mỹ đã khai báo trong ggplot(), bạn có thể khai báo lại hoặc khai báo thuộc tính thẩm mỹ đó bằng NULL.

## Hình bên trái
p1<-dat%>%ggplot(aes(x = life_expectancy, y = gdp_per_capita, 
                color = continent)) + 
  geom_point(alpha = 0.4)+
  geom_smooth(aes(color=NULL), se = FALSE)

## Hình ở giữa
p2<-dat%>%ggplot(aes(x = life_expectancy, y = gdp_per_capita, 
                color = continent)) + 
  geom_point(alpha = 0.4)+
  geom_smooth(color="black", se = FALSE)

## Hình bên phải 

p3<-dat%>%ggplot(aes(x = life_expectancy, y = gdp_per_capita, 
                color = continent)) + 
  geom_point(alpha = 0.4)+
  geom_smooth(aes(color="black") , se = FALSE)

## Vẽ p1, p2, p3 trên cùng một đồ thị
grid.arrange(p1,p2,p3, nrow= 1 , ncol = 3)

Có hai cách để bạn tác động đến các thuộc tính thẩm mỹ của đồ thị trong các hàm geom_*(), đó là dùng ánh xạ thẩm mỹ (mapping) và cài đặt tham số (setting). Khác nhau giữa hai cách này là việc bạn khai báo giá trị của thuộc tính thẩm mỹ bên trong hay bên ngoài hàm aes(). Hãy quan sát đồ thị ở trên:

  • Hình bên trái cho đường cong của mô hình \(loess\) có màu xanh da trời. Khi gọi hàm geom_smooth(), chúng ta đã cho thuộc tính thẩm mỹ \(color\) về giá trị mặc định bằng cách ánh xạ thuộc tính này tới giá trị \(NULL\). Màu xanh da trời là màu mặc định của các đường cong được tạo ra từ geom_smooth().

  • Hình ở giữa chúng ta cài đặt (setting) cấu phần thẩm mỹ \(color\) bằng một giá trị cố định là “black” (màu đen). Do đó đường cong được tạo từ geom_smooth() sẽ có màu đen giống như cài đặt. Bạn đọc chỉ cần sử dụng giá trị màu sắc có ý nghĩa với R để cài đặt cho thuộc tính thẩm mỹ \(color\). Nếu trong một hàm geom_*() vừa có ánh xạ thẩm mỹ được khai báo trong hàm aes() và vừa có cài đặt thuộc tính thẩm mỹ (ngoài hàm aes()), \(ggplot2\) sẽ ưu tiên giá trị nằm ngoài aes().

  • Hình bên tay phải phức tạp hơn một chút. Khác với hình ở giữa, thuộc tính \(color\) được gán cho giá trị “black” bên trong hàm aes(). Bạn đọc có thể thấy rằng đường cong được tạo từ hàm geom_smooth() không có màu đen như hình ở giữa. Khi khai báo thuộc tính trong hàm aes(), chúng ta đã ánh xạ thuộc tính \(color\) của hàm geom_smooth() đến một giá trị kiểu ký tự là “black” chứ không phải cho màu sắc của đường cong nhận giá trị màu tương ứng! Trong hàm geom_point() trước đó đã ánh xạ biến \(continent\) tới thuộc tính thẩm mỹ \(color\), khi chúng ta tiếp tục ánh xạ một biến “black” tới \(color\) trong geom_smooth() thì \(ggplot2\) sẽ hiểu rằng có thêm một giá trị mới cho thuộc tính \(color\) (“black”) thêm vào các giá trị hiện có (tên của 5 châu lục). Điều này giải thích tại sao trong chú giải (legend) của hình bên tay phải có 6 loại thay vì 5 loại như hình ở giữa. Đường cong tạo bởi geom_smooth() có màu xanh lá cây vì giá trị “black” có thứ hạng là 4 khi sắp xếp 6 giá trị ánh xạ tới thuộc tính \(color\) theo thứ tự tăng dần.

Khi cân nhắc sử dụng ánh xạ hay thiết lập giá trị cho các thuộc tính thẩm mỹ, bạn đọc nên cân nhắc về việc có muốn tác động lên thuộc tính thẩm mỹ nữa hay không. Nếu bạn muốn cố định giá trị cho thuộc tính thẩm mỹ, hãy sử dụng thiết lập giá trị. Còn nếu bạn muốn tác động ngược lại lên thuộc tính thẩm mỹ đó, hãy sử dụng ánh xạ thay vì thiết lập giá trị.

Hãy nói một chút về cách chú giải ghi nhận giá trị mới của một thuộc tính thẩm mỹ. Trong hình bên phải, khi chúng ta khai báo giá trị “black” cho thuộc tính \(color\), \(ggplot2\) ghi nhận “black” như một giá trị mới tương đương với tên các Châu lục đã sử dụng trong khai báo trước đó. Cách ghi nhận tên biến mới trong chú giải sẽ rất hữu ích khi chúng ta muốn tạo một đồ thị nhiều lớp và đặt tên cho từng lớp trong phần chú giải của đồ thị. Đồ thị dưới đây so sánh ba phương pháp xây dựng mô hình trên dữ liệu là phương pháp hồi quy tuyến tính thông thường (method = "lm"); hồi quy loess (method = "loess"), và mô hình cộng tính tổng quát (method = "gam")

# So sánh ba phương pháp xây dựng mô hình khác nhau của hàm geom_smooth
dat%>%ggplot(aes(x = life_expectancy, y = gdp_per_capita)) +
  #Layer 1: đồ thị rải điểm
  geom_point(alpha = 0.4)+
  
  # Layer 2: Đường hồi quy tuyến tính
  geom_smooth(aes(color="Hồi quy tuyến tính"), method = "lm" , se = FALSE)+
  
  # Layer 3: Đường hồi quy loess
  geom_smooth(aes(color="Hồi quy loess"), method = "loess" , se = FALSE)+
  
  # Layer 4: Mô hình GAM (generalized additive model)
  geom_smooth(aes(color="Mô hình cộng tính tổng quát"), method = "gam" , se = FALSE)

8.3.2 Các hàm geom_*() cơ bản

Các hình dạng đồ họa, gọi tắt là các \(geoms\), là một cách phổ biến để hiển thị một lớp của một đồ thị mô tả dữ liệ. Ví dụ như sử dụng geom_point() sẽ tạo ra một đồ thị phân tán, trong khi sử dụng geom_line() sẽ tạo ra các đồ thị theo đường. Danh sách các \(geoms\) và các thuộc tính thẩm mỹ bạn đọc có thể tìm thấy trong CHEAT SHEET ở trên. Ở đây chúng tôi chỉ tổng hợp và phân loại lại các \(geoms\) một cách ngắn gọn.

  • Khi mô tả một biến:
    • Mô tả biến rời rạc:
      • geom_bar(): hiển thị phân phối của biến rời rạc dưới dạng các thanh.
    • Mô tả một biến liên tục:
      • geom_histogram(): nhóm dữ liệu liên tục lại vào các nhóm (bin) và hiển thị dưới dạng các thanh.
      • geom_density(): vẽ đường mô tả hàm mật độ xác suất của biến ngẫu nhiên liên tục. Hàm mật độ xác suất được ước lượng bằng phương pháp kernel. Giá trị hàm mật độ tại một điểm \(x\) bất kỳ được tính bằng trung bình giá trị hàm \(K\), được gọi là hàm \(kernel\), tính trên khoảng cách từ điểm \(x\) tới tất cả các quan sát. Nếu \(\hat{f}(x)\) là giá trị hàm mật độ tính tại \(x\) bằng phương pháp kernel thì ta có

\[\begin{align} \hat{f}(x) = \cfrac{1}{nh} \times \sum\limits_{i = 1}^{n} \ K\left( \cfrac{x - x_i}{h} \right) \end{align}\] trong đó \(x_i\) là giá trị quan sát thứ \(i\)\(h\) là được gọi là tham số làm mịn. \(h\) càng lớn thì hàm \(\hat{f}\) sẽ càng mịn. Hàm \(K\) được sử dụng làm hàm kernel mặc định cho geom_density() là hàm mật độ của biến ngẫu nhiên phân phối chuẩn.

  • geom_boxplot(): vẽ đồ thị boxplot của một biến liên tục.
p1<-gapminder%>%filter(year==2011)%>%ggplot(aes(fertility))+
  geom_histogram()
p2<-gapminder%>%filter(year==2011)%>%ggplot(aes(fertility))+
  geom_density()
p3<-gapminder%>%filter(year==2011)%>%ggplot(aes(y=fertility))+
  geom_boxplot()
grid.arrange(p1,p2,p3,nrow=1,ncol=3)
  • Khi mô tả hai biến:
    • Cả hai biến đều là liên tục:
      • geom_point(): đồ thị rải điểm là cách hiệu quả nhất để mô tả hai biến liên tục. Bạn đọc có thể sử dụng cùng với geom_smooth() để mô tả mối quan hệ giữa hai biến. Trong trường hợp biến liên tục nhưng các điểm bị trùng nhau khi hiển thị, bạn đọc có thể sử dụng geom_jitter() thay thế cho geom_point() để hiện thị tốt hơn. geom_jitter() sẽ di chuyển các điểm một cách ngẫu nhiên xung quanh điểm ban đầu để tránh việc hiển thị điểm bị trùng nhau.
      • geom_line(): thường để mô tả hai biến liên tục mà một trong hai biến là kiểu thời gian.
      • geom_text(): tương tự geom_point() nhưng hiển thị biến kiểu ký tự thay vì hiển thị điểm. geom_text() thường sử dụng kết hợp với geom_point(). Lưu ý để khi hiển thị biến kiểu ký tự và điểm không bị trùng nhau là bạn đọc cần phải sử dụng thêm các tham số như \(hjust\), \(vjust\) để điều chỉnh trong hàm geom_text(). Trong nhiều trường hợp, để hiện thị biến kiểu ký tự tốt hơn, chúng tôi sử dụng hàm geom_text_repel() từ thư viện bổ sung \(ggrepel\).
      • geom_label(): tương tự geom_text(). Hàm có thể hiện thị tốt hơn trong thư viện \(ggrepel\) là hàm geom_label_repel(). Bạn đọc quan sát ví dụ dưới đây để thấy các hàm geom_text_repel()geom_label_repel() cho kết quả tốt hơn geom_text()geom_label()
p1<-gapminder%>%filter(year==2011, region=="South-Eastern Asia")%>%
  ggplot(aes(fertility, infant_mortality))+
  geom_point()+geom_text(aes(label = country), vjust=1.1)+
  ggtitle("Sử dụng geom_text()")
p2<-gapminder%>%filter(year==2011, region=="South-Eastern Asia")%>%
  ggplot(aes(fertility, infant_mortality))+
  geom_point()+geom_label(aes(label = country), vjust=0.9)+
  ggtitle("Sử dụng geom_label()")
grid.arrange(p1,p2,nrow=1,ncol=2)
p1<-gapminder%>%filter(year==2011, region=="South-Eastern Asia")%>%
  ggplot(aes(fertility, infant_mortality))+
  geom_point()+geom_text_repel(aes(label = country), vjust=1.1)+
  ggtitle("Sử dụng geom_text_repel()")
p2<-gapminder%>%filter(year==2011, region=="South-Eastern Asia")%>%
  ggplot(aes(fertility, infant_mortality))+
  geom_point()+geom_label_repel(aes(label = country), vjust=0.9)+
  ggtitle("Sử dụng geom_label_repel()")
grid.arrange(p1,p2,nrow=1,ncol=2)

-geom_bin2d()geom_density2d() tương tự như geom_histogram()geom_density() dùng để mô tả phân phối của hai biến liên tục. Khi số lượng quan sát cần hiển thị lớn thì sử dụng đồ thị rải điểm sẽ không hiệu quả. geom_bin2d() chia miền giá trị của từng biến thành các khoảng bằng nhau và đếm trong mỗi hình chữ nhật có bao nhiêu điểm sau đó sử dụng màu sắc từ đậm đến nhạt để mô tả số lượng điểm trong từng hình chữ nhật từ nhỏ đến lớn. geom_density2d() sử dụng phương pháp kernel để tính giá trị hàm mật độ trong không gian hai chiều. Khoảng cách từ quan sát \(x_i\) đến điểm \(x\) được sử dụng là khoảng cách Euclid. Kernel được sử dụng là hàm mật độ của biến ngẫu nhiên phân phối chuẩn hai chiều. geom_density2d() vẽ các đường nỗi các điểm có giá trị hàm mật độ bằng nhau. Hình vẽ dưới đây mô tả phân phối đồng của hai biến \(favorite\_count\)\(retweet\_count\) trong dữ liệu \(trump\_tweet\).

# geom_bind2d
p1<-trump_tweets%>%mutate(log_favorite_count = log(favorite_count), 
                          log_retweet_count = log(retweet_count)) %>%
  ggplot(aes(log_favorite_count,log_retweet_count))+geom_bin2d()+theme_minimal()

# geom_density2d
p2<-trump_tweets%>%mutate(log_favorite_count = log(favorite_count), 
                          log_retweet_count = log(retweet_count)) %>%
  ggplot(aes(log_favorite_count,log_retweet_count))+geom_density2d()+theme_minimal()

grid.arrange(p1,p2,nrow=1, ncol = 2)
  • Một biến liên tục và một biến rời rạc
    • geom_boxplot() vẽ đồ thị boxplot của một biến liên tục theo các nhóm được phân loại theo biến rời rạc.
    • geom_violin() tương tự geom_boxplot() và sử dụng cùng với geom_boxplot() để bổ sung thông tin khi mô tả phân phối của biến liên tục trong từng nhóm. Chúng ta có thể mô tả phân phối của biến \(life_expectancy\) trong các năm 1990, 2000, và 2010 trong dữ liệu \(gapminder\) như sau
p1<- gapminder%>%filter(year %in% c(1990,2000,2010))%>%ggplot(aes(x = as.factor(year),y = life_expectancy))+geom_boxplot()

p2<-gapminder%>%filter(year %in% c(1990,2000,2010))%>%ggplot(aes(x = as.factor(year),y = life_expectancy))+geom_violin()

grid.arrange(p1,p2,nrow=1, ncol = 2)
  • Mô tả hai biến rời rạc:
    • geom_count() được sử dụng để mô tả hai biến rời rạc. geom_count() tạo ra đồ thị thường được gọi là đồ thị kiểu bong bóng, mà kích thước của mỗi bong bóng cho biết số lượng hay mật độ của các điểm rời rạc. Ví dụ như khi mô tả hai biến là \(cut\)\(color\) trong dữ liệu \(diamonds\), chúng ta sử dụng geom_count() như sau
diamonds%>%ggplot(aes(cut,color))+geom_count(color="lightskyblue")+theme_minimal()

Không khó để nhận ra tỷ trọng lớn các viên kim cương có biến \(cut\) nhận giá trị \(ideal\), và trong các viên kim cương đó màu \(G\) có tỷ trọng lớn nhất.

  • Mô tả ba biến: Sử dụng các hình ảnh kiểu 3 chiều không phải là một phương pháp tốt để hiển thị ba biến trên cùng một đồ thị. \(ggplot2\) thường hiển thị dữ liệu trên một mặt phẳng và sử dụng một thuộc tính thẩm mỹ nào đó làm chiều thứ ba. geom_title() thường được sử dụng để mô tả ba biến cùng một lúc bằng cách sử dụng trục \(x\)\(y\) đề mô tả hai biến và màu sắc để mô tả biến thứ ba. Hình vẽ dưới đây sử dụng geom_tile() để mô tả ba biến \(region\), \(year\), và \(life\_expectancy\). \(life\_expectancy\) được tính theo trung bình của vùng qua các năm.
danhsach<-gapminder%>%filter(year==2010)%>%
  group_by(region)%>%summarise(continent = unique(continent))%>%
  arrange(continent)
gapminder%>%group_by(year,continent, region)%>%summarise(life_expectancy = mean(life_expectancy,na.rm = TRUE))%>%
  ggplot()+geom_tile(aes(x = year, y = region , fill = life_expectancy), color= "grey")+
  scale_fill_gradientn(colours = c(rgb(0.8,0.3,0.3),rgb(0.9,0.9,0.9),rgb(0.3,0.3,0.9)))+
  scale_x_continuous(breaks = seq(1960,2010,5))+
  scale_y_discrete(limits = danhsach$region)+
  theme_minimal()

Trong đồ thị ở trên chúng tôi đã sử dụng thêm các hàm scale_*() để kiểm soát ánh xạ thẩm mỹ. Chẳng hạn giá trị năm (trục x) sẽ là cách đều 5 năm, các vùng trên trục \(y\) được sắp xếp theo lục địa (Châu Đại Dương, Châu Âu, Châu Á, Châu Mỹ, và cuối cùng là Châu Phi). Dải màu sắc cũng được cho giá trị dải màu từ đỏ sang xanh da trời. Chúng ta sẽ thảo luận về scale_*() trong phần sau của chương sách.

8.3.3 Các hàm stat_*()

Bạn đọc cũng có thể xây dựng các lớp cho đồ thị \(ggplot2\) bằng các hàm stat_*(). Các hàm số này thường không hiển thị dữ liệu ở trạng thái ban đầu mà thường hiển thị dữ liệu dưới một phép biến đổi thống kê hoặc một sau một tóm tắt dữ liệu theo một cách nào đó. Nhiều hàm stat_*() được gọi thông qua các hàm geom_*(), chẳng hạn như stat_bin() tương đương với geom_histogram()geom_bar(); stat_smooth() tương đương với geom_smooth(); … Sự tương đồng hay khác biệt giữa sử dụng hàm stat_*() và hàm geom_*() sẽ được thảo luận ở phần cuối của chương sách.

Thay vì sử dụng geom_*(), chúng ta có thể sử dụng stat_*() để mô tả phân phối của các biến liên tục

# ĐỒ thị histogram
p1<-gapminder%>%filter(year==2011)%>%ggplot(aes(fertility))+
  stat_bin()+ggtitle("Đồ thị histogram của fertility") 
# hàm phân phối xác suất
p2<-gapminder%>%filter(year==2011)%>%ggplot(aes(fertility))+
  stat_density()+ggtitle("Hàm mật độ") 
p3<-gapminder%>%filter(year==2011)%>%ggplot(aes(fertility))+
  stat_boxplot()+ggtitle("Đồ thị boxplot") 
grid.arrange(p1,p2,p3,nrow=1,ncol=3)

8.4 Scale

\(Scale\) trong ggplot2 kiểm soát ánh xạ từ dữ liệu đến thuộc tính thẩm mỹ của đồ thị. Các hàm scale_*() lấy dữ liệu ban đầu và biến đổi thành các đối tượng trực quan mà bạn có thể nhìn thấy, như kích thước, màu sắc, vị trí hoặc hình dạng. Bạn có thể tạo biểu đồ bằng \(ggplot2\) mà không cần biết chính xác ánh xạ hoạt động như thế nào, nhưng hiểu về \(scale\) và học cách thao tác các hàm scale_*() sẽ giúp bạn kiểm soát tốt hơn rất nhiều.

8.4.1 Vị trí xuất hiện trên trục tọa độ

Đa số các đồ thị trong \(ggplot2\) hiển thị dữ liệu trên trục tọa độ Descartes nên chúng tôi sẽ tập trung vào cách dữ liệu hiển thị trên hai trục tọa độ \(x\)\(y\). Khi ánh xạ các cột dữ liệu tới các trục tọa độ \(x\)\(y\) nếu chúng ta không sử dụng \(scale\), các điểm sẽ được hiển thị đúng như giá trị của điểm đó trên các trục tọa độ. Trong nhiều trường hợp, hiển thị tại vị trí đúng như dữ liệu ban đầu sẽ không mang lại hiệu quả. Ví dụ như khi mô tả hai biến \(total\)\(population\) của dữ liệu \(murders\), bạn đọc có thể so sánh cách hiển thị giữa việc không kiểm soát (hình bên trái) và có kiểm soát ánh xạ thẩm mỹ như hình dưới đây

p1<-murders%>%ggplot(aes(x = population,y = total))+geom_point(size = 3, color = "lightskyblue")+
  theme_minimal()+ggtitle("Không sử dụng scale")
p2<-murders%>%ggplot(aes(x = population,y = total))+geom_point(size = 3, color = "lightskyblue")+
  theme_minimal()+
  scale_x_continuous(trans = "log10")+
  scale_y_continuous(trans = "log10")+
  ggtitle("Có sử dụng scale (log10)")
grid.arrange(p1,p2,nrow=1,ncol=2)

Có thể thấy rằng hình bên phải hiển thị rõ ràng hơn hình bên trái sau khi chúng ta kiểm soát ánh xạ từ biến \(population\) đến thuộc tính \(x\) và từ biến \(total\) đến thuộc tính \(y\) bằng các hàm scale_x_continuous()scale_y_continuous(). Đây là hai hàm số được dùng để kiểm soát vị trí xuất hiện của các điểm trên trục tọa độ khi các biến trong ánh xạ là các biến kiểu số liên tục. Các tham số có thể được sử dụng trong các hàm này bao gồm có:

  • Tham số \(trans\), là viết tắt của transformation, nhận giá trị mặc định là ‘identity’ nghĩa là lấy chính xác giá trị của biến ánh xạ vào thuộc tính \(x\) hoặc \(y\) tương ứng. Để biết các giá trị mà tham số này có thể nhận được bạn đọc có thể tham khảo trong tài liệu đi kèm với hàm scale_x_continuous()scale_y_continuous(). Khi scale các biến, chẳng hạn như \(x_1\)\(x_2\), là các biến ánh xạ tới \(x\)\(y\) trong ánh xạ thẩm mỹ bằng một hàm \(f\) được khai báo bằng tham số \(trans\), giá trị xuất hiện trên trục tọa độ \(x\)\(y\) sẽ tương ứng là \(f(x_1)\)\(f(x_2)\). Chẳng hạn như trong đồ thị phía bên phải ở trên, khi chúng ta thực hiện scale sử dụng hàm \(log10\), tọa độ của các điểm (các quốc gia) sẽ là \(log10(population)\)\(log10(total)\). Việc chuyển đổi này sẽ hữu ích bởi rất đa số các quốc gia có dân số nhỏ, trong khi có một vài quốc gia có dân số rất lớn. Thực hiện chuyển đổi dữ liệu bằng các hàm \(log\) sẽ giúp cho khoảng cách của các điểm cách đều nhau hơn và dễ dàng phân biệt hơn.

  • Tham số \(limits\) giới hạn giá trị trên các trục \(x\)\(y\). Mỗi khi chúng ta vẽ đồ thị sử dụng \(ggplot2\), tham số \(limits\) mặc định sẽ đảm bảo việc hiển thị được đầy đủ nhất. Tuy nhiên, khi chúng ta muốn so sánh hai dữ liệu con trên cùng một miền giá trị của \(x\)\(y\), sử dụng tham số \(limits\) cho phép so sánh rõ ràng hơn là sử dụng tham số mặc định. Hình vẽ dưới đây mô tả hai biến \(fertility\)\(life\_expectancy\) trong năm 1960 và 2010 và không sử dụng scale.

Không thể thấy rõ được sự khác biệt giữa hai năm 1960 và 2010 nếu không biểu diễn các biến trên cùng một miền giá trị của \(x\)\(y\). Hình phía dưới sử dụng tùy biến \(limits\). Để khai báo tham số cho tùy biến này chúng ta sử dụng một véc-tơ hai chiều chứa giá trị nhỏ nhất và giá trị lớn nhất trên trục mà bạn muốn hiển thị. Rõ ràng đã có sử khác nhau đáng kể về sự phân bố của các điểm trong năm 1960 và năm 2010.

p1<-gapminder%>%filter(year==1960)%>%
  ggplot(aes(fertility,life_expectancy))+
  geom_point(shape = 21, size = 3,alpha = 0.8, fill= "lightskyblue")+
  ggtitle("Năm 1960")+
  scale_x_continuous(limits = c(1,9))+
  scale_y_continuous(limits = c(25,85))
p2<-gapminder%>%filter(year==2010)%>%
  ggplot(aes(fertility,life_expectancy))+
  geom_point(shape = 21, size = 3,alpha = 0.8, fill= "lightskyblue")+
  ggtitle("Năm 2010")+
   scale_x_continuous(limits = c(1,9))+
  scale_y_continuous(limits = c(25,85))
grid.arrange(p1,p2,nrow=1,ncol=2)
  • Tham số \(breaks\) kiểm soát vị trí các điểm đánh dấu xuất hiện trên các trục \(x\) và trục \(y\). Chúng tôi thường kết hợp \(breaks\) với tham số \(labels\) để kiểm soát đồng thời vị trí và cách hiển thị trên các trục số. Ví dụ như trong hình so sánh đồ thị rải điểm của năm 1960 và 2010 chúng ta muốn giá trị xuất hiện trên các trục \(x\) là các số 2, 4, 6, 8 thay vì 2,5; 5,0; và 7, và các số trên trục \(y\) xuất hiện tại các vị trí 10, 30, 50, 70, và 90 thay vì 40, 60, 80 như hiện tại, chúng ta chỉ cần gán giá trị tham số \(breaks\) bằng véc-tơ chứa các giá trị mà chúng ta muốn hiển thị. Lưu ý rằng \(breaks\) có chữ \(s\) ở cuối để phân biệt với từ khóa \(break\).
p1<-gapminder%>%filter(year==1960)%>%
  ggplot(aes(fertility,life_expectancy))+
  geom_point(shape = 21, size = 3,alpha = 0.8, fill= "lightskyblue")+
  ggtitle("Năm 1960")+
  scale_x_continuous(limits = c(1,9), 
                     breaks = c(2,4,6,8),
                     labels = paste(c(2,4,6,8),"trẻ em"))+
  scale_y_continuous(limits = c(25,85), 
                     breaks = c(10,30,50,70,90),
                     labels = paste(c(10,30,50,70,90),"tuổi"))
p2<-gapminder%>%filter(year==2010)%>%
  ggplot(aes(fertility,life_expectancy))+
  geom_point(shape = 21, size = 3,alpha = 0.8, fill= "lightskyblue")+
  ggtitle("Năm 2010")+
  scale_x_continuous(limits = c(1,9), 
                     breaks = c(2,4,6,8),
                     labels = paste(c(2,4,6,8),"trẻ em"))+
  scale_y_continuous(limits = c(25,85), 
                     breaks = c(10,30,50,70,90),
                     labels = paste(c(10,30,50,70,90),"tuổi"))
grid.arrange(p1,p2,nrow=1,ncol=2)

Khi một trong hai biến liên tục là biến kiểu thời gian thì hàm số sử dụng để kiểm soát giá trị hiển thị là scale_x_date() với hai tham số thường được sử dụng là \(date\_break\)\(date\_labels\). Bạn đọc quan sát dữ liệu \(AirPassengers\) được trực quan hóa khi sử dụng scale_x_date() như sau

dat<-data.frame(Number_Passengers = AirPassengers, 
                Month = seq(as.Date("1949-01-01"), by = "month", length.out = 144))
p1<-dat%>%ggplot(aes(x = Month, y = Number_Passengers))+
  geom_line() + ggtitle("Không sử dụng scale")
p2<-dat%>%ggplot(aes(x = Month, y = Number_Passengers))+
  geom_line()+ ggtitle("Sử dụng scale_x_date()")+
  scale_x_date(date_break = "2 years", date_labels = "%b\n%Y" )+
  scale_y_continuous(breaks = seq(100,600,length=6))
                     
grid.arrange(p1,p2,nrow=1,ncol=2)

Khi giá trị trên trục \(x\) hoặc trục \(y\) là các giá trị rời rạc, các hàm số sử dụng để kiểm soát ánh xạ thẩm mỹ từ biến đến các trục tọa độc là scale_x_discrete()scale_y_discrete(). Các tham số thường sử dụng bao gồm \(limits\)\(labels\). Tham số \(limits\) được sử dụng để cho biết các giá trị nào của biến rời rạc xuất hiện trên đồ thị, trong khi tham số \(labels\) cho biết từng giá trị của biến rời rạc xuất hiện như thế nào

p1<-murders%>%mutate(rate = total/population*10^6)%>%
  ggplot(aes(region,rate))+
  geom_boxplot()+ggtitle("Không sử dụng scale")

# Sử dụng tham số limits cho giá trị trên trục x
p2<-murders%>%mutate(rate = total/population*10^6)%>%
  ggplot(aes(region,rate))+
  geom_boxplot()+ggtitle("Không sử dụng scale")+
  scale_y_continuous(limits = c(0,50))+
  # chỉ hiển thị boxplot cho 2 vùng "Northeast" và "West"
  scale_x_discrete(limits = c("Northeast", "West"))+
  ggtitle("Sử dụng dụng tham số limits")
# Sử dụng tham số labels cho giá trị trên trục x
p3<-murders%>%mutate(rate = total/population*10^6)%>%
  ggplot(aes(region,rate))+
  geom_boxplot()+ggtitle("Không sử dụng scale")+
  scale_y_continuous(limits = c(0,100))+
  # Thay thế giá trị hiển thị trên trục số bằng labels
  scale_x_discrete(labels = c("Northeast" = "Đông Bắc", 
                              "West" = "Miền Tây",
                              "South" = "Miền Nam",
                              "North Central" = "Miền Bắc"))+
  ggtitle("Sử dụng dụng tham số labels")
grid.arrange(p1,p2,p3,nrow=1,ncol=3)

8.4.2 Màu sắc hiển thị và chú giải

Thuộc tính thẩm mỹ được sử dụng phổ biến nhất là màu sắc. Có nhiều cách để ánh xạ giá trị của biến tới màu sắc trong \(ggplot2\). Vì màu sắc là một chủ đề phức tạp nên phần này chúng tôi sẽ bắt đầu bằng việc thảo luận một chút về lý thuyết màu sắc. Sau đó chúng tôi sẽ giới thiệu đến bạn đọc về thang màu liên tục, thang màu rời rạc và thang màu tổng hợp. Chúng tôi cũng sẽ đề cập đến các thang màu dành cho biến kiểu thời gian ngày/giờ, độ trong của các màu sắc hiển thị và nguyên tắc chú giải được thiết lập trong các đồ thị \(ggplot2\).

8.4.2.1 Cảm nhận về màu sắc

Trong vật lý, màu sắc được tạo ra bởi hỗn hợp các bước sóng ánh sáng. Để mô tả đầy đủ về một màu sắc, chúng ta cần biết sự kết hợp chính xác của các bước sóng. Sự thật thì mắt con người chỉ có ba cơ quan cảm nhận màu sắc khác nhau, và vì vậy chúng ta có thể tóm tắt khả năng cảm nhận về bất kỳ màu nào chỉ bằng ba con số. Một không gian màu có thể quen thuộc với bạn đọc là không gian màu RGB, không gian mà mọi màu sắc được xác định theo cường độ ánh sáng đỏ, xanh da trời và xanh lá cây để tạo ra màu đó. Ưu điểm của không gian màu này là sự đơn giản do mỗi màu sắc đều được mô tả bằng ba con số từ 0 đến 255 hoàn toàn độc lập với nhau. Một vấn đề với không gian này là các dải màu liên tục nhận được bằng cách tăng giảm các cường độ màu đỏ, xanh lam, xanh lá cây lại không giống như cách nhận thức về màu sắc của còn người. Khi nhìn vào một màu cụ thể, chúng ta không thể ước tính được cường độ mỗi màu là bao nhiêu, điều này có thể gây khó khăn cho việc tạo ánh xạ từ một biến liên tục sang một dải màu.

Mỗi khi hiển thị một giá trị màu sắc trong không gian RGB, R thường sử dụng ký tự có 6 chữ số trong hệ 16 (từ 0 đến F) và bắt đầu bằng một dấu ‘#’ thay vì một véc-tơ ba chiều đại diện cho 3 sắc đỏ, xanh lá cây, và xanh lam. Hai chữ số đầu đại diện cho sắc đỏ, 2 chữ số tiếp theo đại diện cho màu xanh lá cây và 2 chữ số cuối đại diện cho màu lam. Chẳng hạn như “#FF0000” sẽ là màu đỏ, “#00FF00” là màu xanh lá cây và “#0000FF” là màu xanh lam.

Một không gian màu được chuyển đổi từ không gian RGB là không gian Lab trong đó L đại diện cho độ tương phản sáng-tối của màu sắc, trục tọa độ a và b cho biết các vị trí của màu trên trên trục đỏ đến xanh lam và vàng đến xanh lá. Cải tiến từ không gian RGB sang không gian Lab giúp cho các dải màu sắc tương ứng hơn với cách khả năng nhận biết màu sắc của con người, tuy nhiên vẫn còn khoảng cách giữa không gian Lab với nhận thức màu sắc. Không gian \(Lab\) cũng có các ưu điểm riêng, do đó \(ggplot2\) mặc định sử dụng không gian \(Lab\) khi nội suy tuyến tính các màu sắc nằm giữa hai màu bất kỳ khi chúng ta ánh xạ một biến liên tục lên thuộc tính thẩm mỹ màu sắc.

Một không gian màu khác có thể hạn chế vấn đề của không gian RGB là không gian màu HCL với ba thành phần màu: màu sắc (Hue), sắc độ (Chroma) và độ chói (Luminance):

  • Màu sắc nằm trong khoảng từ 0 đến 360 (một góc) và cho biết màu muốn hiển thị.

  • Sắc độ là “độ tinh khiết” của một màu, nằm trong khoảng từ 0 (xám) đến mức tối đa thay đổi theo độ sáng.

  • Độ sáng là độ sáng của màu, dao động từ 0 (đen) đến 1 (trắng).

Ba chiều có những đặc tính khác nhau. Tương tự như không gian màu Lab, màu sắc trong HCL được sắp xếp xung quanh một hình tròn và không được coi là có trật tự; ví dụ: màu xanh lá dường như không lớn hơn hay nhỏ hơn màu đỏ và màu xanh lam dường như không lớn hơn hay nhỏ hơn màu xanh lá. Ngược lại, cả sắc độ và độ sáng đều được coi là có trật tự: màu hồng được coi là nằm giữa màu đỏ và trắng, và màu xám được coi là nằm giữa màu đen và trắng. Tạo các thang màu sắc từ không gian HCL thường được dựa trên nguyên tắc cố định 2 tham số và thay đổi tham số còn lại. Do không gian màu HCL gần với nhận thức màu sắc của con người hơn nên các dải màu được tạo ra sẽ “cách đều” nhau hơn theo cách mà chúng ta nhận thức.

Xin được nhắc lại rằng màu sắc là một chủ để phức tạp mà phạm vi của nó vượt rất xa những gì mà chúng tôi đề cập ở trên. Bạn đọc nên tham khảo thêm các tài liệu chuyên ngành khoa học máy tính để có thể sử dụng màu sắc một cách hiệu quả nhất.

8.4.2.2 Dải màu liên tục

Dải màu liên tục được sử dụng để hiển thị giá trị của một biến liên tục trên bề mặt phẳng. Để kiểm soát màu sắc trong \(ggplot2\), chúng ta sử dụng các hàm scale_color_*(). Lưu ý rằng các thuộc tính thẩm mỹ \(color\)\(fill\) là tương đồng nhau, do đó bất kỳ hàm scale_color_*() cũng có hàm scale_fill_*() tương ứng.

Dải màu liên tục thường được sử dụng cùng với các \(geoms\) có hình dạng đồ họa cần màu sắc để phân biệt trên trên mặt phẳng như geom_polygon(), geom_tile() (hoặc geom_raster()), và geom_bin2d(). Mỗi khi chúng ta cho một biến liên tục ánh xạ đến thuộc tính thẩm mỹ màu sắc, \(ggplot2\) sẽ tự động hiểu rằng chúng ta sử dụng dải màu liên tục để mô tả biến đó. Hình vẽ dưới đây mô tả hàm mật độ của biến ngẫu nhiên phân phối chuẩn hai chiều trung bình 0, phương sai 1 và hệ số tương quan \(\rho = 0.8\). Lưu ý rằng hàm mật độ của hai biến ngẫu nhiên phân phối chuẩn \(\mathcal{N}(0,1)\) với hệ số tương quan \(\rho = 0.8\) được tính như sau \[\begin{align} f(x,y) = \cfrac{1}{2 \pi \sqrt{1-\rho^2}} \ \exp \left(- \cfrac{x^2 + y^2 - 2\rho x y}{1-\rho^2} \right) \end{align}\]

# tạo lưới điểm trên hình vuông [-2,2] * [2-,2]
n<-100
x<-rep(1:n,n)/n*4-2 
y<-sort(x, decreasing = FALSE)
rho<-0.8
dat<-data.frame(x=x,y=y,dens = 1/(2*pi*sqrt(1-rho^2)) * exp(-(x^2+y^2-2*rho*x*y)/(1-rho^2)))
p<-dat%>%ggplot(aes(x,y,fill=dens))+geom_raster()+theme_minimal()

Phương pháp đơn giản nhất để kiểm soát ánh xạ thẩm mỹ từ một biến liên tục đến màu sắc là lựa chọn các dải màu liên tục có sẵn trong \(ggplot2\) hoặc trong các thư viện bổ sung. Các dải màu có sẵn này đều được xây dựng để những người gặp khó khăn trong phân biệt màu sắc cũng có thể cảm nhận được. Trong hình vẽ dưới đây chúng tôi lựa chọn các dải màu: 1. Dải màu mặc định của \(ggplot2\), 2. Dải màu \(viridis\), 3. Dải màu \(distiller\) và 4. Dải màu \(fermenter\). Mỗi dải màu sẽ có tùy biến \(palette\) để lựa chọn.

# tạo lưới điểm trên hình vuông [-2,2] * [2-,2]
p1<-p + scale_fill_continuous()+
  ggtitle("Màu mặc định") # sử dụng dải màu mặc định
p2<-p + scale_fill_viridis_c()+ # Dải màu viridis liên tục
  ggtitle("Dải màu viridis")
p3<-p + scale_fill_distiller()+ # Dải màu distiller
  ggtitle("Dải màu distiller")
p4<-p + scale_fill_fermenter()+ # Dải màu fermenter
  ggtitle("Dải màu fermenter")
grid.arrange(p1,p2,p3,p4,nrow=2,ncol=2)

Để dải màu sắc liên tục có tính cá nhân hóa cao hơn, bạn đọc cần chỉ định thang màu sắc thay vì sử dụng các thang màu có sẵn. \(Gradient\) \(scale\) là một công cụ mạnh mẽ giúp bạn thực hiện việc này. Bạn chỉ cần cung cấp các giá trị màu sắc tương ứng với giá trị nhỏ nhất, giá trị lớn nhất, có thể thêm một vài giá trị trung gian, \(ggplot2\) sẽ nội suy tuyến tính ra các màu sắc trong thang màu. Các hàm số có thể sử dụng để tạo thang màu bao gồm

  • scale_fill_gradient() tạo một thang màu liên tục giữa hai màu sắc mà bạn khai báo. Hai tham số được sử dụng để khai báo hai điểm đầu của dải màu là tham số \(low\) và tham số \(high\). Mỗi khi chúng ta ánh xạ tuyến tính từ một biến liên tục đến màu sắc, \(ggplot2\) mặc định sử dụng dải màu liên tục theo hàm số này với giá trị low là “#132B43” và giá trị high là “#56B1F7”. Không gian để nội suy tuyến tính là không gian màu Lab.

  • scale_fill_gradient2() tạo một thang màu liên tục từ ba màu, bao gồm một màu sắc ở giữa. Ngoài hai giá trị là hai điểm đầu của hai thang màu, chúng ta cần khai báo thêm một màu ở giữa bằng tham số \(mid\) và tham số \(midpoint\) cho biết giá trị nào của biến ánh xạ tới thuộc tính màu sắc tương ứng với màu được khai báo với tham số \(mid\). Nếu không khai báo tham số \(midpoint\) sẽ nhận giá trị mặc định là 0.

  • scale_fill_gradientn() tạo một thang màu liên tục từ một véc-tơ chứa các màu sắc khai báo.

p1<-p + ggtitle("Màu mặc định") # sử dụng dải màu mặc định
p2<-p + scale_fill_gradient(low = "blue", high = "red")+
  ggtitle("Dải màu từ xanh lam đến đỏ") # sử dụng dải màu từ xanh lam đến đo
p3<-p + scale_fill_gradient2(low = "blue", high = "red", mid = "white", midpoint = 0.12)+
  ggtitle("Dải màu từ xanh lam đến đỏ điểm giữa là trắng") 
p4<-p +  scale_fill_gradientn(colours = c("#00FF00","#FFFFFF","#0000FF", "#FFFF00"))+
  ggtitle("Dải màu đi qua nhiều điểm màu") 
grid.arrange(p1,p2,p3,p4,nrow=2,ncol=2)

Cả ba hàm số kể trên đều nội suy tuyến tính trong không gian màu \(Lab\) để tạo ra các giải màu liên tục. Khi nói đến nội suy tuyến tính giữa hai màu sắc, sẽ dễ hiểu nếu chúng ta sử dụng không gian RGB mà tất cả các màu đều nằm trong một hình lập phương với điểm (0,0,0) là màu đen, (1,1,1) là màu trắng… Bạn đọc có thể hiểu như sau: trong mỗi không gian mỗi màu sắc hiển thị có ba thành phần là cường độ màu đỏ (r), cường độ màu xanh lá (g) cường độ màu xanh lam (b) … Một dải màu bao gồm \(n\) màu, bắt đầu từ màu \(m_1\) bao gồm các thành phần \((r_1, g_1, b_1)\), đến màu \(m_n\) với thành phần \((r_n, g_n, b_n)\) sẽ là các màu \(m_i\) có các thành phần tương ứng \[\begin{align} r_i = \left[r_1 + (i-1) * \cfrac{r_n - r_1}{(n-1)} \right] \\ g_i = \left[g_1 + (i-1) * \cfrac{g_n - g_1}{(n-1)} \right] \\ b_i = \left[b_1 + (i-1) * \cfrac{b_n - b_1}{(n-1)} \right] \end{align}\]

Đáng tiếc là trong không gian \(Lab\) việc nội suy màu sắc không đơn giản như vậy. Việc nội suy dựa trên các tính toán phức tạp và kết quả cuối cùng là các công thức gần đúng. Ưu điểm của nội suy màu sắc trong không gian \(Lab\) so với không gian \(RGB\) sự chuyển đổi màu sắc giữa các điểm mượt mà hơn rất nhiều trong cách nhận biết màu sắc của con người.

Cả hai hình đều sử dụng dải màu liên tục từ xanh lam đến đỏ để mô tả một biến liên tục là mật độ của phân phối chuẩn hai chiều có hệ số tương quan \(\rho=0\), hình bên trái nội suy trong không gian RGB, hình bên phải nội suy trong không gian Lab. Có thể thấy rằng việc chuyển hóa màu sắc từ xanh lam sang đỏ khi sử dụng không gian màu Lab là mượt hơn nhiều so với không gian RGB.

Tương tự như vị trí trên trục tọa độ, các tham số \(limits\), \(breaks\), và \(label\) cũng có thể được sử dụng trong các hàm \(scale_fill_*()\) để kiểm soát các thang màu liên tục. Tham số \(limits\) nhận giá trị là một véc-tơ hai phần tử, phần tử thứ nhất cho biết màu sắc bắt đầu trong thang màu tương ứng với giá trị nào trong biến liên tục và phần tử thứ hai cho biết màu sắc kết thúc của thang màu ứng với giá trị nào của biến liên tục. Hàm \(breaks\)\(labels\) sử dụng để thay đổi giá trị trên thang màu của chú giải.

# limits cho biết hai giá trị tương ứng với điểm đầu và cuối của dải màu
p1<-p + scale_fill_gradient(low = "blue", high = "red", 
                            limits = c(0,0.5))+
  ggtitle("Tham số limits")
# breaks cho biết các giá trị nào xuất hiện trên chú giải
# labels cho biết giá trị hiển thị trong chú giải
p2<-p + scale_fill_gradient(low = "blue", high = "red", 
                            limits = c(0,0.3),
                            breaks = c(0.1,0.15,0.25),
                            labels = paste("Density at", c(0.1,0.15,0.25)))+
  ggtitle("Tham số breaks và labels") 
grid.arrange(p1,p2,nrow=1,ncol=2)

8.4.2.3 Dải màu rời rạc

Dải màu rời rạc dùng để mô tả thuộc tính thẩm mỹ màu sắc của các biến rời rạc. Hàm số dùng để kiểm soát màu sắc rời rạc trong \(ggplot2\)scale_fill_discrete()scale_color_discrete(). Mỗi khi sử dụng các hàm số kiểm soát màu sắc rời rạc, \(ggplot2\) sẽ mặc định sử dụng dải màu rời rạc “cách đều nhau” trong không gian \(HCL\). Dải màu mặc định này có cùng sắc độ (Chromes hay tham số \(c\)), độ sáng (Luminance hay tham số \(l\)) và giá trị \(h\) cách đều nhau từ góc 15 độ (\(h\) nhận giá trị từ 0 đến 360 độ). Bạn đọc muốn sử dụng các dải màu rời rạc trong không gian \(hcl\) thì có thể sử dụng scale_fill_hue()scale_color_hue() thay vì scale_fill_discrete()scale_color_discrete().

p1<-gapminder%>%filter(year==2011)%>%
  ggplot(aes(continent,fill=continent))+geom_bar()
p2<-gapminder%>%filter(year==2011)%>%
  ggplot(aes(continent,fill=continent))+geom_bar()+
  scale_fill_hue(h=c(0,360)+15+360/5)
p3<-gapminder%>%filter(year==2011)%>%
  ggplot(aes(continent,fill=continent))+geom_bar()+
  scale_fill_hue(c=30)
grid.arrange(p1,p2,p3,nrow=1,ncol=3)

Dải màu mặc định đối với biến rời rạc sử dụng tham số \(c\) bằng 100 và tham số \(l\) bằng 65 trong khi tham số \(h\) nhận các giá trị cách đều nhau, bắt đầu từ \(h = 15 (độ)\). Lưu ý rằng \(h\) nhận giá trị từ 0 độ đến 360 độ nên trong trường hợp biến rời rạc có năm giá trị, các màu sắc sẽ lần lượt nhận các giá trị h = 15, 15 + 360/5, 15 + 2 * 360/5, 15 + 3 * 360/5 và 15 + 4 * 360/5. Đó là màu sắc của các thanh trong đồ thị barplot bên tay trái theo thứ tự từ trái qua phải. Trong hình ở giữa, khi chúng ta sử dụng giá trị tịnh tiến giá trị h lên 360/5, chúng ta có thể thấy các màu sắc bắt đầu từ h = 15 + 360/5 và kết thúc ở h = 15. Trong hình bên phải, chúng tôi giảm độ chói (tham số \(c\)) xuống còn 40. Chúng ta có thể thấy dải màu vẫn tương tự như hình ban đầu nhưng không đạt được độ sáng như vậy.

Bạn đọc cũng có thể sử dụng các dải màu rời rạc được thiết kế sẵn cho mục đích trực quan hóa các biến rời rạc. Dải màu rời rạc mà chúng tôi thường sử dụng là dải màu Brewer. Những dải màu này được thiết kế để hoạt động tốt trong nhiều tình huống khác nhau kể cả đối với những người khó khăn khi nhận biết màu sắc hay khi sử dụng để hiển thị trên những bề mặt lớn. Hàm số để kiểm soát ánh xạ thẩm mỹ màu sắc sử dụng dải màu Brewer là scale_color_brewer()scale_fill_brewer(). Bạn đọc cần sử dụng thư viện \(RColorBrewer\) để gọi được các hàm này. Để xem các dải màu có sẵn trong thư viện này, bạn đọc sử dụng câu lệnh sau

display.brewer.all()

Bạn đọc sử dụng tùy biến \(palette\) trong hàm scale_color_brewer() để lựa chọn dải màu có sẵn.

p1<-gapminder%>%filter(year==2011)%>%
  ggplot(aes(continent,fill=continent))+geom_bar()+
  scale_fill_brewer(palette = "Dark2")+
  ggtitle("Sử dụng dải màu Dark2")
p2<-gapminder%>%filter(year==2011)%>%
  ggplot(aes(continent,fill=continent))+geom_bar()+
  scale_fill_brewer(palette = "Set1")+
  ggtitle("Sử dụng dải màu Set1")
p3<-gapminder%>%filter(year==2011)%>%
  ggplot(aes(continent,fill=continent))+geom_bar()+
  scale_fill_brewer(palette = "Spectral")+
  ggtitle("Sử dụng dải màu Spectral")
grid.arrange(p1,p2,p3,nrow=1,ncol=3)

Để tạo ra dải màu rời rạc theo ý muốn của mình, bạn đọc sử dụng hàm scale_fill_manual()scale_color_manual(). Tham số \(values\) trong hàm này nhận giá trị là véc-tơ chứa màu sắc mà bạn đọc tự tạo. Lưu ý rằng số lượng phần tử trong véc-tơ phải tương ứng với số lượng phần tử trong biến rời rạc.

Một hàm số có thể được sử dụng để nội suy ra các màu sắc “cách đều nhau” trong không gian màu “RGB” hoặc không gian màu Lab là hàm số colorRampPalette() của thư viện \(grDevices\). Để tạo ra một véc-tơ có độ dài 5, mỗi giá trị là một màu sắc được nội suy tuyến tính từ màu xanh lam đến màu đỏ chúng ta sử dụng colorRampPalette() như sau

# nội suy trong RGB
mypalette1<-colorRampPalette(c("blue","red"), space = "rgb")(5)
# nội suy trong Lab
mypalette2<-colorRampPalette(c("blue","red"), space = "Lab")(5)

Các đồ thị barplot dưới đây sử dụng các màu sắc mà chúng ta tự chỉ định bằng hàm scale_fill_manual()

p1<-gapminder%>%filter(year==2011)%>%
  ggplot(aes(continent,fill=continent))+geom_bar()+
  scale_fill_manual(values = c("blue","green","grey","yellow","red"))+
  ggtitle("Màu tự định nghĩa")
p2<-gapminder%>%filter(year==2011)%>%
  ggplot(aes(continent,fill=continent))+geom_bar()+
  scale_fill_manual(values = mypalette1)+
  ggtitle("Màu nội suy trong RGB")
p3<-gapminder%>%filter(year==2011)%>%
  ggplot(aes(continent,fill=continent))+geom_bar()+
  scale_fill_manual(values = mypalette2)+
  ggtitle("Màu nội suy trong Lab")
grid.arrange(p1,p2,p3,nrow=1,ncol=3)

Cách sử dụng tham số \(limits\), \(breaks\), và \(label\) cũng gần tương tự như đối với biến liên tục. Tham số \(limits\) cho biết các giá trị nào trong biến rời rạc được ánh xạ tới dải màu sắc. Tham số \(breaks\) cho biết các giá trị nào không được sử dụng trong ánh xạ thẩm mỹ. \(label\) cho biết cách các màu sắc hiển thị trong phần chú giải. Theo kinh nghiệm của chúng tôi thì tham số \(breaks\) không có nhiều ý nghĩa khi sử dụng đối với dải màu sắc liên tục, trong khi tham số \(limits\) có ý nghĩa quan trọng khi bạn đọc cần cố định ánh xạ màu sắc lên biến rời rạc khi vẽ nhiều biểu đồ khác nhau và để kiểm soát thứ tự xuất hiện của biến liên tục trên đồ thị.

p1<-gapminder%>%filter(year==1960, continent == "Asia")%>%
  arrange(-population)%>%head(10)%>%
  ggplot(aes(fill=country))+
  geom_bar(aes(x = population, y = reorder(country,population)),stat="identity",col="black")+
  ylab("")+ggtitle("Năm 1960")+
  scale_x_continuous(labels = scales::comma)+
  scale_fill_manual(values = c("blue","red","yellow"), limits = c("Philippines","Vietnam", "Indonesia"))
p2<-gapminder%>%filter(year==2010, continent == "Asia")%>%
  arrange(-population)%>%head(10)%>%
  ggplot(aes(fill=country))+
  geom_bar(aes(x = population, y = reorder(country,population)),stat="identity",col="black")+
  ylab("")+ggtitle("Năm 2010")+
  scale_x_continuous(labels = scales::comma)+
  scale_fill_manual(values = c("blue","red","yellow"), limits = c("Philippines","Vietnam", "Indonesia"))

grid.arrange(p1,p2,nrow=1,ncol=2)

Sử dụng \(limits\) trong đồ thị ở trên giúp chúng ta nhấn mạnh vào 3 quốc gia Philippines, Vietnam, và Indonesia trong nhóm 10 nước có dân số lớn nhất châu Á trong các năm 1960 và 2010.

8.4.3 Các thuộc tính thẩm mỹ khác

Ngoài vị trí và màu sắc, còn có một số thuộc tính thẩm mỹ khác mà \(ggplot2\) có thể sử dụng để mô tả dữ liệu. Trong phần này, chúng ta sẽ xem xét thuộc tính kích thước (size), hình dạng (shape), chiều rộng của line và kiểu line, sử dụng cùng với các thuộc tính vị trí và màu sắc để thể hiện tốt nhất các biến trong dữ liệu. Ngoài đề cập đến các giá trị mặc định, chúng tôi cũng sẽ thảo luận về các hàm số để bạn đọc có thể sử dụng để kiểm soát tốt các thuộc tính này.

8.4.3.1 Kích thước (size)

Thuộc tính thẩm mỹ kích thước thường được sử dụng để mô tả hình dạng đồ họa kiểu điểm hoặc ký tự. Như chúng tôi đã đề cập trong phần giới thiệu, thuộc tính kích thước thường được sử dụng với biến liên tục. Nếu không có hàm kiểm soát ánh xạ thẩm mỹ, bán kính của điểm tương ứng với giá trị nhỏ nhất luôn là 1 và bán kính của điểm có giá trị lớn nhất luôn là 6, nghĩa là có bán kính gấp 6 lần bán kính của điểm nhỏ nhất. Khi nội suy ra kích thước của các điểm khác, \(ggplot2\) mặc định cho kích thước của điểm là diện tích của hình tròn mô tả điểm đó chứ không phải đường kính của hình tròn. Kích thước của điểm sẽ phụ thuộc vào thứ hạng (rank) của giá trị đó trong biến liên tục chứ không được tính bằng giá trị thực của điểm đó. Nếu \(r_m\) là bán kính của hình tròn tương ứng với giá trị nhỏ nhất và \(r_M\) tương ứng với diện tích của hình tròn tương ứng với giá trị lớn nhất thì diện tích của hình tròn tương ứng với giá trị có thứ hạng \(k\) trong tổng số \(n\) giá trị của biến liên tục là \[\begin{align} area = r_m + (k-1) \times \cfrac{r_M - r_m}{n - 1} \end{align}\]

Bạn đọc có thể quan sát kích thước của các hình tròn trong hình dưới đây

dat<-data.frame(x=1:3,y=1:3,z=1:3)
# Hình bên trái
p1<-dat%>%ggplot(aes(x,y,size=z))+geom_point(shape=21,fill= "lightskyblue")+
  theme(legend.position = "none")
# Hình ở giữa
p2<-dat%>%ggplot(aes(x,y,size=z^2))+geom_point(shape=21,fill= "lightskyblue")+
  theme(legend.position = "none")
# Hình bên phải
p3<-dat%>%filter(z>=2)%>%ggplot(aes(x,y,size=z))+geom_point(shape=21,fill= "lightskyblue")+
  theme(legend.position = "none")
grid.arrange(p1,p2,p3,ncol=3,nrow=1)

Hình bên tay trái: diện tích của hình tròn nằm ở tọa độ (2,2) bằng trung bình cộng diện tích của hình tròn nằm ở vị trí (1,1) và (3,3). Do diện tích của hình tròn nằm ở vị trí (3,3) bằng \(6^2 = 36\) lần diện tích của hình tròn tại vị trí \((1,1)\) nên diện tích của hình tròn tại (2,2) bằng \(\cfrac{36+1}{2} = 18,5 \textit{(lần)}\) diện tích hình tròn tại (1,1), hay nói cách khác đường kính của hình tròn tại vị trí (2,2) bằng \(\sqrt{18,5} \sim 4,3 \textit{ (lần)}\) đường kính của hình tròn tại vị trí (1,1). Hình ở giữa: cho thấy khi chúng ta ánh xạ thuộc tính thẩm mỹ vào \(z^2\) thay vì \(z\) thì kích thước các hình tròn vẫn không hề thay đổi do thứ hạng của các điểm trong véc-tơ \(z\) không thay đổi. Hình bên phải: khi chúng ta chỉ vẽ hai điểm thay vì cả ba điểm, diện tích hình tròn nhỏ nhất và hình tròn lớn nhất vẫn không thay đổi.

Hàm số dùng để kiểm soát giá trị của ánh xạ thẩm mỹ kích thước là hàm scale_size(). Để thay đổi miền giá trị của kích thước, chúng ta sử dụng tham số \(range\).

dat<-data.frame(x=1:3,y=1:3,z=1:3)
# Hình bên trái
p1<-dat%>%ggplot(aes(x,y,size=z))+geom_point(shape=21,fill= "lightskyblue")+
  theme(legend.position = "none")
# Hình ở giữa
p2<-dat%>%ggplot(aes(x,y,size=z))+geom_point(shape=21,fill= "lightskyblue")+
  scale_size(range=c(1,12))+
  theme(legend.position = "none")
# Hình bên phải
p3<-dat%>%ggplot(aes(x,y,size=z))+geom_point(shape=21,fill= "lightskyblue")+
  scale_size(range=c(6,24))+
  theme(legend.position = "none")
grid.arrange(p1,p2,p3,ncol=3,nrow=1)

Hình bên trái: đường kính của hình nhỏ nhất là 1, của hình lớn nhất là 6. Hình ở giữa: đường kính của hình nhỏ nhất là 1, của hình lớn nhất là 12. Hình bên phải: đường kính của hình nhỏ nhất là 6, của hình lớn nhất là 24. Đường kính của các hình nằm ở giữa được nội suy tuyến tính theo diện tích tăng dần theo hạng của điểm đó.

Trong trường hợp bạn đọc muốn sử dụng nội suy tuyển tính theo đường kính của điểm thay vì diện tích, hãy sử dụng hàm scale_radius()

dat<-data.frame(x=1:3,y=1:3,z=1:3)
# Hình bên trái
p1<-dat%>%ggplot(aes(x,y,size=z))+geom_point(shape=21,fill= "lightskyblue")+
  theme(legend.position = "none")+
  scale_size(range=c(1,7))
# Hình ở giữa
p2<-dat%>%ggplot(aes(x,y,size=z))+geom_point(shape=21,fill= "lightskyblue")+
  scale_radius(range=c(1,7))+
  theme(legend.position = "none")
# Hình bên phải
p3<-dat%>%ggplot(aes(x,y,size=z))+geom_point(shape=21,fill= "lightskyblue")+
  scale_radius(range=c(4,10))+
  theme(legend.position = "none")
grid.arrange(p1,p2,p3,ncol=3,nrow=1)

Hình bên trái sử dụng scale theo diện tích và đường kính hình tròn lớn nhất bằng 7 lần đường tròn nhỏ; hình tròn ở giữa có bán kính bằng \(\sqrt{\cfrac{7^2+1^2}{2}} = 5 \textit{ (lần)}\) diện tích hình tròn nhỏ nhất. Hình ở giữa, do scale theo đường kính hình tròn nên hình ở giữa có đường kính bằng \(\cfrac{7+1}{2} = 4 \textit{ (lần)}\) đường kính hình tròn nhỏ. Hình bên phải: đường kính của hình nhỏ nhất là 4, hình lớn nhất là 10, nên bán kính hình ở giữa là \(\cfrac{4+10}{2} = 7\) (bằng kích thước của hình tròn lớn nhất của hình ở giữa).

Các tham số \(limits\), \(breaks\), và \(label\) được sử dụng tương tự như thuộc tính thẩm mỹ màu sắc. \(limits\) cho biết miền giá trị nào của biến được ánh xạ đến thuộc tính thẩm mỹ size. \(breaks\) cho biết các kích thước nào xuất hiện trên chú giải, và \(labels\) mô tả thuộc tính thẩm trên chú giải của đồ thị.

gapminder%>%filter(year==2010)%>%
  ggplot(aes(infant_mortality,life_expectancy, size = population))+
  geom_point(shape = 21, fill = "lightskyblue",alpha = 0.5)+
  scale_size(range = c(1,12),
             limits = c(10^7,max(gapminder$population)),
             breaks = c(10^8,2*10^8,5*10^8,10^9),
             #label = paste("Dân số",c(10^8,2*10^8,5*10^8,10^9)),
             labels = scales::label_comma())

8.4.3.2 Hình dạng (shape)

Hình dạng thường được sử dụng để mô tả một biến rời rạc có không quá nhiều giá trị riêng biệt. Theo kinh nghiệm của tác giả thì hình dạng chỉ nên sử dụng với các biến có nhỏ hơn hoặc bằng 5 giá trị riêng biệt. Mặc dù \(ggplot2\) cho phép sử dụng lên đến hơn 20 hình dạng khác nhau nhưng sử dụng nhiều hơn 5 hình dạng trong một đồ thị sẽ làm cho đồ thị trở nên rắc rối và khó khăn trong nhận diện. Tại phiên bản \(ggplot2\) mà tác giả đang sử dụng, có 25 hình dạng khác nhau có thể dùng để mô tả biến rời rạc ứng với 25 số tự nhiên từ 1 đến 25 như sau

dat<-data.frame(x=c(rep(1:10,2),1:5),y = c(rep(3,10),rep(2,10),rep(1,5)), z = 1:25)
dat%>%ggplot(aes(x,y,shape=as.factor(z)))+geom_point(size=3)+
  scale_shape_manual(values = 1:25)+theme_classic()+geom_text(aes(label=z),vjust=-1)+
  theme(legend.position = "none")+
  theme_void()

Bạn đọc lưu ý rằng có một số hình dạng trông giống nhau nhưng lại có thuộc tính thẩm mỹ khác nhau. Chẳng hạn như hình dạng tương ứng với số 1 là một điểm hình tròn với thuộc tính thẩm mỹ color là màu sắc của hình tròn đó, trong khi hình dạng tương ứng với số 21 có thuộc tính thẩm mỹ color là màu viền bên ngoài của hình tròn và thuộc tính thẩm mỹ fill mới là màu sắc bên trong hình tròn.

Để kiểm soát ánh xạ thẩm mỹ đến thuộc tính hình dạng, bạn đọc sử dụng hàm scale_shape_manual().

gapminder%>%filter(year==2010, continent == "Asia")%>%
  ggplot(aes(gdp/population,life_expectancy, shape = region))+
  geom_point()+
  scale_x_continuous(trans="log10")+
  scale_shape_manual(values=c(21:24,8) )

Nhìn chung ánh xạ biến rời rạc đến thuộc tính thẩm mỹ hình dạng không cho hiệu quả tốt trong phân biệt các nhóm. Bạn đọc nên thận trọng khi sử dụng thuộc tính thẩm mỹ này.

8.4.3.3 Kích thước và hình dạng của các đường (linewidth và linetype)

Đối với hình dạng đồ họa kiểu các đường như geom_line(), geom_path(), geom_segment() chúng ta có thể ánh xạ các biến rời rạc vào độ rộng hoặc hình dạng của đường. Hình vẽ dưới đây mô tả sự thay đổi của biến \(gdp\) của ba quốc gia bao gồm Mỹ, Trung Quốc và Nhật Bản theo thời gian từ năm 1960 đến năm 2010.

gapminder%>%filter(country %in% c("United States", "China", "Japan"), year <= 2011)%>%
  ggplot(aes(x = year,y = gdp/10^9))+
  geom_line(aes(linetype = country))+
  theme_minimal()+
  ylab("GDP in $B")+
  scale_x_continuous(breaks = seq(1960,2010,10))+
  scale_y_continuous(labels = scales::label_comma())

Hàm số dùng để kiểm soát ánh xạ thẩm mỹ vào hình dạng của đường là scale_linetype_manual(). \(ggplot2\) có 13 hình dạng cho các đường được đánh số từ 1 đến 13 như dưới đây

Để các đường có hình dạng như mong muốn, chúng ta gán giá trị tham số \(values\) cho véc-tơ chứa các số từ 1 đến 13 là hình dạng mà bạn lựa chọn.

gapminder%>%filter(country %in% c("United States", "China", "Japan"), year <= 2011)%>%
  ggplot(aes(x = year,y = gdp/10^9))+
  geom_line(aes(linetype = country))+
  theme_minimal()+
  ylab("GDP in $B")+
  scale_x_continuous(breaks = seq(1960,2010,10))+
  scale_y_continuous(labels = scales::label_comma())+
  scale_linetype_manual(values = c(4,7,12))

8.5 Chú giải của ánh xạ thẩm mỹ

Về mặt hình thức, nếu coi các hàm scale_*() trong phần trước của chương sách như các ánh xạ từ một tập hợp các giá trị của biến đến một tập hợp các giá trị của thuộc tình thẩm mỹ thì chú giải là ánh xạ ngược từ thuộc tính thẩm mỹ đến miền giá trị của biến. Chú giải cho phép bạn chuyển đổi các thuộc tính trực quan trở lại giá trị của dữ liệu. Giá trị xuất hiện trên các trục tọa độ và các chú giải có cách hiển thị khác nhau nhưng về bản chất lại có cùng một mục đích là cho phép người tiếp nhận quan sát các hình ảnh đồ họa trực quan và ánh xạ chúng trở lại giá trị của dữ liệu.

Chú giải có khả năng giải thích tốt hơn giá trị xuất hiện trên các trục tọa độ bởi các nguyên nhân sau

  • Chú giải có thể giải thích nhiều biến cùng lúc trong khi giá trị trên trục tọa độ chỉ cho phép một biến.

  • Chú giải có thể tùy biến dễ hơn: có thể xuất hiện ở các vị trí theo ý muốn của người xây dựng đồ thị, có thể xuất hiện theo bất kỳ hướng nào.

Bạn đọc hãy lưu ý rằng dù chúng ta không gọi bất kỳ hàm scale_*() nào trong các câu lệnh thì \(ggplot2\) vẫn luôn luôn sử dụng một hàm \(scale\) mặc định để ánh xạ từ biến đến miền giá trị của thuộc tính thẩm mỹ. Mỗi khi bạn gọi hàm scale để kiểm soát ánh xạ thẩm mỹ, các giá trị mà bạn khai báo sẽ thay thế cho các giá trị mặc định. Trong trường hợp bạn gọi nhiều hàm scale tác động đến một thuộc tính thẩm mỹ thì chỉ có hàm scale_*() sau cùng bạn gọi ra sau cùng được sử dụng.

p<-gapminder%>%filter(year==2010,region=="South-Eastern Asia")%>%
  mutate(gdp_per_capita = gdp/population)%>%
  ggplot(aes(reorder(country,gdp_per_capita),gdp_per_capita,fill = country))+
  geom_bar(stat="identity")+
  theme_minimal()
p+scale_y_continuous(name = "GDP bình quân đầu người", labels = scales::label_comma())+
  scale_x_discrete(name = "Country")+
  scale_y_continuous(trans = "sqrt")+
  scale_x_discrete(name = "Quốc gia", labels = c(
    "Vietnam" = "VN",
    "Thailand" = "TL",
    "Timor-Leste" = "Đông Timor"))+
  scale_y_continuous(name = "GDP bình quân đầu người", labels = scales::label_dollar())

Khi bạn sử dụng nhiều hàm scale_*() tác động đến cùng một thuộc tính thẩm mỹ như trên, \(ggplot2\) sẽ đưa ra các cảnh báo. Bạn cần xem xét lại các câu lệnh của mình để đảm bảo sử dụng đúng với mục đích.

Nhìn chung để kiểm soát chú giải của các ánh xạ thẩm mỹ, bạn đọc sử dụng tham số \(guide\) trong các hàm scale_*() tương ứng. Giá trị gán cho tham số \(guide\) là một trong các hàm số sau đây:

  • guide_axis() là hàm số dùng để gán cho tham số \(guide\) khi chúng ta sử dụng các hàm scale_*() nhằm kiểm soát ánh xạ thẩm mỹ đến các trục tọa độ.
p+ scale_x_discrete(name = "Quốc gia",
                   guide = guide_axis(title = "Country",
                                      angle = 90))+
  scale_y_continuous(name = "GDP bình quân đầu người", labels = scales::label_dollar(),
                     guide = guide_axis(title = "GDP per capita"))

Bạn đọc có thể thấy rằng tham số \(title\) trong hàm \(guide\) đã thay thế cho tham số \(name\) trong hàm scale_(). Tham số \(angle\) cho biết hướng các giá trị xuất hiện trên trục tọa độ. Bạn đọc tham khảo hướng dẫn sử dụng hàm guide_axis() để hiểu về các tham số khác như \(n.dodge\), \(order\), hay \(position\).

  • guide_legend() là hàm số dùng để gán cho tham số \(guide\) khi gọi các hàm scale_*() kiểm soát ánh xạ từ các biến rời rạc đến màu sắc. Có rất nhiều tham số có thể sử dụng trong hàm số này. Bạn đọc tham khảo hướng dẫn sử dụng hàm để biết đầy đủ các tham số.
p+scale_x_discrete(guide = guide_axis(title = "Quốc gia",
                                      angle = 90))+
  scale_y_continuous(labels = scales::label_dollar(),
                     guide = guide_axis(title = "GDP per capita"))+
  scale_fill_brewer(palette = "Paired",
                    guide = guide_legend(
                      title = "Quốc gia",
                      title.position = "top",
                      ncol = 2
                    ))
  • guide_colorbar() được dùng khi chú giải cho các ánh xạ từ biến liên tục đến dải màu liên tục

  • guide_bin() được dùng khi chú giải cho các ánh xạ từ biến liên tục đến thuộc tính thẩm mỹ kích thước (size).

8.6 Các kiểu trục tọa độ

8.7 Chủ đề và ngữ cảnh của đồ thị (theme)

Trong phần này, chúng ta sẽ học cách sử dụng chủ đề và ngữ cảnh (theme) cho các đồ thị. Ngữ cảnh cho phép bạn đọc kiểm soát tốt các cấu phần không ánh xạ đến dữ liệu trong câu chuyện của bạn. Nhìn chung chủ đề và ngữ cảnh không ảnh hưởng đến cách dữ liệu được hiển thị bằng các hình dạng đồ họa hoặc cách dữ liệu được biến đổi. Chủ đề và ngữ cảnh cho phép bạn đọc kiểm soát những cấu phần như phông chữ, hình nền, vị trí chú giải,…

Sự phân tách giữa các thành các phần có ánh xạ đến dữ liệu và thành phần không ánh xạ đến dữ liệu trong \(ggplot2\) là điểm khác biệt so với đồ họa cơ sở. Trong đồ họa cơ sở hầu hết các hàm đều có một số lượng lớn các tham số số chỉ định cả hình thức dữ liệu và phần không liên quan đến dữ liệu, điều này làm cho các hàm trong đồ thị cơ sở trở nên phức tạp. \(ggplot2\) tiếp cận theo cách khác: khi tạo đồ thị, bạn xác định cách hiển thị dữ liệu trước, sau đó bạn có thể chỉnh sửa mọi chi tiết không liên quan đến dữ liệu bằng cách hàm kiểm soát chủ đề và ngữ cảnh. Để kiểm soát chủ đề và ngữ cảnh của đồ thị, bạn đọc cần nắm vững các nội dung sau:

  • Các chủ đề và ngữ cảnh đã được hoàn chỉnh sẵn có trong \(ggplot2\) và trong thư viện \(ggthemes\).

  • Kiểm soát các thành phần của chủ đề và ngữ cảnh như: tiêu đề của đồ thị (kiểu chữ, kích thước, vị trí), cách hiển thị các số trên các trục, cách hiển thị các hình dạng đồ họa trên chú giải, kiểu chữ, kích thước hay vị trí của chú giải…

  • Kiểm soát các tùy biến của các hàm dùng để gán giá trị cho các thành phần của chủ đề. Ví dụ như hàm element_text() có thể dùng để chỉnh kích thước phông chữ, màu sắc và giao diện của các thành phần văn bản.

  • Cách sử dụng hàm theme() với một danh sách dài các tùy biến cho phép bạn ghi đè lên các thành phần của chủ đề và ngữ cảnh mặc định.

8.8 Tạo đồ thị tương tác và đồ thị động.

Các đồ thị của thư viện \(ggplot2\) đều là các đồ thị tĩnh. Các đồ thị động hay đồ thị tương tác ngoài lợi thế hơn đồ thị tĩnh ở việc thu hút thị giác của người tiếp nhận còn ở khả năng mô tả dữ liệu một cách đầy đủ thông tin hơn:

  • Các đồ thị dạng động đặc biệt hiệu quả trong việc mô tả sự thay đổi dữ liệu theo thời gian.

  • Các đồ thị tương tác cho phép hiển thị thông tin bằng con trỏ, hoặc phóng to, thu nhỏ từng phần của đồ thị. Bạn đọc tránh phải hiển thị quá nhiều thông tin lên đồ thị cùng lúc.

Khuyết điểm duy nhất của các đồ thị tương tác và các đồ thị động đó là không thể biểu diễn trên các bản in cứng.

Trong phần này của chương sách, chúng tôi sẽ thảo luận về hai thư viện dùng để tạo đồ thị tương tác và đồ thị dạng động sử dụng cùng với \(ggplot2\)\(ggiraph\)\(plotly\). Nếu như \(ggiraph\) là thư viện bổ sung cho \(ggplot2\) và được xây dựng dựa trên cấu trúc ngữ pháp đồ thị tương tự như \(ggplot2\) thì \(plotly\) là một thư viện khá độc lập với \(ggplot2\) và chuyên sử dụng để tạo đồ thị dạng động và tương tác.

8.8.1 Tạo đồ thị tương tác với \(ggiraph\)

Ưu điểm lớn nhất của \(ggiraph\) đó là các câu lệnh tạo đồ thị cũng được dựa trên ngữ pháp của đồ thị, nghĩa là hoàn toàn tương đồng với các câu lệnh trong \(ggplot2\). Để tạo một đồ thị trong \(ggiraph\), bạn đọc chỉ cần thêm các thuộc tính thẩm mỹ của đồ thị tương tác và đồ thị động cùng với các thuộc tính của đồ thị tĩnh của \(ggplot2\). Tại thời điểm chúng tôi viết chương sách này, thư viện \(ggiraph\) đang ở phiên bản 0.8.7 và hướng dẫn sử dụng ở trong link dưới đây

https://cloud.r-project.org/web/packages/ggiraph/ggiraph.pdf

Sau khi xem qua danh sách các hàm số trong thư viện \(ggiraph\), bạn đọc có thể thấy rằng đa số các hàm geom_*() trong thư viện \(ggplot2\) đều có một hàm tương ứng để tạo đồ thị tương tác là geom_*_interactive(). Chẳng hạn như hàm geom_point() trong thư viện \(ggplot2\) sẽ có hàm tương ứng trong \(ggiraph\)geom_point_interactive(). Hai cấu phần thẩm mỹ cho đồ thị tương tác là \(tooltip\)\(data_id\). Cũng giống như \(plotly\), bạn đọc cần tạo một đối tượng kiểu đồ thị bằng hàm ggplot() sau đó sử dụng hàm girafe() để tạo đồ thị tương tác. Hãy quan sát ví dụ dưới đây:

p<-murders %>% ggplot(aes(y = total, x = population)) +
  geom_point_interactive(aes(fill=region, 
                             tooltip = paste0("State: ", state, "\n Region: ", region, "\n Population: ", population), 
                             onclick = region), 
                         size = 4, shape=21, alpha = 0.8, color = "black") +
  geom_smooth(method = "lm", se = FALSE, linetype = 2, color="grey")+
  scale_x_continuous(trans = "log10", labels = scales::label_comma()) +
  scale_y_log10() +
  scale_fill_brewer(palette = "Dark2")+
  theme_minimal()+
  ggtitle("Số vụ sát nhân bằng súng tại các bang năm 2010")
girafe(ggobj = p)

Thuộc tính thẩm mỹ \(tooltip\) cho biết thông tin hiển thị của các điểm trên đồ thị khi sử dụng con trỏ trong khi thuộc tính thẩm mỹ \(data\_id\) khi được ánh xạ đến từ một biến sẽ cho biết các quan sát có cùng giá trị trên biến đó. Bạn đọc có thể sử dụng con trỏ di chuyển đến từng các điểm để xem kết quả của ánh xạ đến thuộc tính \(tooltip\)\(data_id\).

Cách sử dụng các thuộc tính thẩm mỹ \(tooltip\)\(data\_id\) hoàn toàn tương tự trong các đồ thị cơ bản khác.

  1. Đồ thị dạng bong bóng
p<-diamonds%>%group_by(cut,color)%>%mutate(ave_price = mean(price))%>%ungroup()%>%
  as.data.frame()%>%
  ggplot(aes(cut,color,color = ave_price))+
  geom_count_interactive(aes(tooltip = paste0("Number: ", after_stat(n))))+
  scale_color_continuous(type = "viridis")+
  scale_size(range=c(1,12))+
  theme_minimal()
girafe(ggobj = p)
  1. Đồ thị dạng line: Đồ thị dưới đây mô tả tỷ lệ thất nghiệp của nước Mỹ qua các thời kỳ Tổng thống và các Đảng
dat1<-presidential[3:11,]
p<-economics%>%mutate(unemploy_rate = unemploy/pop)%>%
  ggplot()+
  geom_line_interactive(aes(x=date,y=unemploy_rate,tooltip = name, data_id = party))+
  scale_y_continuous(limits = c(0.013,0.052))+
  geom_rect(data=dat1,
              aes(xmin = start, xmax = end, 
                  ymin = 0.013, ymax = 0.052,fill = party),alpha = 0.4)+
  geom_rect_interactive(data=dat1,
            aes(xmin = start, xmax = end, 
                ymin = 0.013, ymax = 0.052,
                tooltip = name,
                data_id = name),color = "black",size=0.1,alpha=0.01)+
  scale_fill_manual(values=c("blue","red"))+
  theme_minimal()
girafe(ggobj = p)
  1. Đồ thị dạng thanh

  2. Bản đồ tương tác

Chúng ta sẽ mô tả biến infant_mortality của dữ liệu \(gapminder\) thông qua bản đồ thế giới

8.8.2 Tạo đồ thị tương tác với \(plotly\)

Để tạo một đồ thị tương tác bằng thư viện \(plotly\) dễ hơn bạn nghĩ rất nhiều. Việc duy nhất bạn đọc cần làm là gọi thư viện \(plotly\) và sau đó sử dụng hàm ggplotly(). Đoạn câu lệnh dưới đây mô tả dữ liệu \(murders\) dưới dạng đồ thị rải điểm tương tác.

p<-murders %>% ggplot(aes(y = total, label = state, x = population)) +
  geom_point(aes(fill=region), size = 4, shape=21, alpha = 0.8, color = "black") +
  geom_smooth(method = "lm", se = FALSE, linetype = 2, color="grey")+
  scale_x_continuous(trans = "log10", labels = scales::label_comma()) +
  scale_y_log10() +
  scale_fill_brewer(palette = "Dark2")+
  theme_minimal()+
  ggtitle("Số vụ sát nhân bằng súng tại các bang năm 2010")
ggplotly(p)

Bạn đọc có thể tương tác với đồ thị bằng các thao tác như sau:

  1. Sử dụng con trỏ chỉ vào các điểm để xem thông tin chính xác về dân số, số vụ sát nhân, tên bang, và tên vùng của mỗi điểm.

  2. Sử dụng con trỏ, hoặc các nút phóng to, thu nhỏ để xem từng phần của đồ thị.

  3. Sử dụng con trỏ trên chú giải để lựa chọn các vùng nào hiển thị, hoặc không hiển thị trên đồ thị.

  4. Sử dụng con trỏ trượt theo đường thẳng tạo bởi geom_smooth() để biết giá trị trên trục total và population của mỗi điểm trên đường thẳng. Lưu ý rằng giá trị xuất hiện là giá trị sau khi đã chuyển đổi bằng hàm \(log10()\).

Các thông tin bạn đọc muốn hiển thị bằng con trỏ là các dữ liệu đã được ánh xạ vào trong các thuộc tính thẩm mỹ của đồ thị. Trong đồ thị ở trên thông tin của mỗi điểm bao gồm có 1: Population, 2: Total, 3: State, và 4: region. Nếu không sử dụng \(plotly\), thuộc tính thẩm mỹ \(label\) sẽ không được hiển thị do chúng ta không sử dụng geom_text() hay geom_label(). \(plotly\) sẽ hiển thị thông tin của tất cả các biến có ánh xạ đến thuộc tính thẩm mỹ, dù thuộc tính thẩm mỹ đó không hiển thị trong \(ggplot2\).

Tham số \(tooltip\) trong hàm \(ggplotly\) được sử dụng để kiểm soát các thuộc tính thẩm mỹ xuất hiện trên đồ thị tương tác. Ví dụ như trong đồ thị rải điểm ở trên, bạn đọc không chỉ muốn thông tin hiển thị trên mỗi điểm chỉ bao gồm tên bang (thuộc tính thẩm mỹ \(fill\)) và vùng (thuộc tính thẩm mỹ \(label\)), chúng ta sử dụng tùy biến \(tooltip\) như sau

# Thông tin mỗi điểm chỉ bao gồm 1. Tên bang (label) và 2. Vùng (fill) 
ggplotly(p, tooltip = c("label","fill"))

Chúng ta có thể sử dụng \(ggplotly\) trên hầu hết các đồ thị được tạo bởi \(ggplot2\), dưới đây là một số ví dụ

  1. Đồ thị kiểu bong bóng:
p<-diamonds%>%group_by(cut,color)%>%mutate(ave_price = mean(price))%>%ungroup()%>%
  as.data.frame()%>%
  ggplot(aes(cut,color,color = ave_price))+
  geom_count()+
  scale_color_continuous(type = "viridis")+
  scale_size(range=c(1,12))+
  theme_minimal()
ggplotly(p, tooltip = c("n", "fill"))
  1. Đồ thị kiểu line
p<-gapminder%>%filter(country %in% c("United States","Japan","China","Germany","France"),
                   year <= 2011)%>%mutate(gdp_bil_usd = gdp/10^9)%>%
  ggplot(aes(x = year, y = gdp_bil_usd, color = country, linetype = country))+
  geom_line(size=0.5)+
  scale_y_continuous(labels = scales::label_comma())+
  theme_minimal()
ggplotly(p, tooltip = c("x","y", "color") )
  1. Đồ thị kiểu thanh: barplot kết hợp với thuộc tính thẩm mỹ \(fill\) có thể sử dụng để trực quan hóa hai biến rời rạc. Đồ thị dưới đây mô tả thu nhập bình quân đầu người tại các Châu lục vào năm 2010 theo các mức độ: dưới $2000, từ $2000 đến $5000, và trên $5000.
p<-gapminder%>%filter(year == 2010)%>%drop_na()%>%
  mutate(gdp_per_capita = gdp/population,
         gdp_levels = ifelse(gdp_per_capita<2000,"Low",
                            ifelse(gdp_per_capita<5000,"Medium","High")),
         gdp_range = factor(gdp_levels, levels = c("High","Medium","Low")))%>%
  ggplot(aes(x = continent,fill = gdp_range))+
  geom_bar(color="grey",alpha=0.6)+
  scale_fill_manual(values = c("green","blue","red"))+
  theme_minimal()
ggplotly(p, tooltip = "count")
  1. Bản đồ tương tác: bản đồ tương tác giúp cho việc hiển thị dữ liệu trên bản đồ trở nên đơn giản hơn rất nhiều so với sử dụng geom_text() hoặc geom_label()
dat<-map_data("state")
dat1<-murders%>%mutate(murder_rate=total/population*10^6,
                       state = tolower(state))

p<-dat%>%mutate(state = region)%>%
  mutate(murder_rate=dat1$murder_rate[match(state,dat1$state)])%>%
  ggplot(aes(x=long,y=lat,group=group,label = state, fill=murder_rate))+
  geom_polygon(color="black",size = 0.1)+
  scale_x_continuous(expand=c(0,0))+
  scale_fill_gradientn(colors = c(rgb(0.95,0.95,0.95),rgb(0.95,0.3,0.3),
                                  rgb(0.95,0.1,0.1)))+
  theme_minimal()+
  theme(legend.position = "bottom")
ggplotly(p)

Tạo đồ thị dạng động (dynamic) là một phương pháp thường được sử dụng để mô tả dữ liệu biến đổi theo thời gian. Đồ thị dạng động ngoài yếu tố bắt mắt còn giúp cho người tiếp nhận dữ liệu cảm nhận được bản chất của vấn đề phức tạp một cách trực quan nhất. Hãy bắt đầu với một dữ liệu đơn giản bao gồm hai biến \(x\), \(y\) và thời gian \(time\).

dat<-data.frame(x=1:10,y=1:10,time=1:10)

Chúng ta muốn vẽ một điểm tại các vị trí lưu ở cột \(x\) và cột \(y\) di chuyển theo thời gian được lưu trong cột \(time\) với \(ggplotly\), chúng ta chỉ cần khai báo thêm ánh xạ thẩm mỹ từ thuộc tính \(frame\) của hàm animation_opts() đến biến \(time\) như dưới đây

p<-dat%>%ggplot(aes(x=x,y=y,size=time, frame = time, color= time))+
  geom_point()+
  theme_minimal()
ggplotly(p, width = 600, height = 600, tooltip = "color") %>%
animation_slider(frame = 1000)

Đồ thị dạng động sẽ được kích hoạt mỗi khi chúng ta bấm nút “play”. Các tham số \(width\)\(height\) trong hàm ggplotly() cho biết kích thước của đồ thị dạng động trong khi tham số \(frame\) trong hàm animation_opts() cho biết độ mượt của hình động.

Giả sử bạn đọc muốn theo dõi mối quan hệ giữa hai biến tuổi thọ trung bình và tỷ lệ sinh trung bình của tất cả các quốc gia trên thế giới. Bạn sử dụng đồ thị rải điểm để mô tả mối quan hệ giữa hai biến liên tục, sử dụng màu sắc để phân biệt giữa các châu lục, sử dụng kích thước của các điểm để mô tả dân số, và sau cùng bạn sử dụng \(frame\) để mô tả biến \(year\). Với một vài điều chỉnh ánh xạ thẩm mỹ, bạn đã có thể kể được một câu chuyện hấp dẫn về tuổi thọ trung bình và tỷ lệ sinh dựa trên dữ liệu \(gapminder\)

p<-gapminder%>%filter(year %in% 1960:2011)%>%
  ggplot(aes(x = fertility, y = life_expectancy, size = population, 
             fill = continent, frame = year, label = country))+
  geom_point(alpha = 0.5,shape=21)+
  scale_fill_brewer(palette = "Set1")+
  scale_size(range=c(1,15))+
  theme_minimal()+
  ggtitle("Tuổi thọ và tỷ lệ sinh trung bình 1960 đến 2011")
ggplotly(p, width = 800, height = 600, tooltip = c("label","size") ) %>%
animation_slider(frame = 204)

Một trong những công việc khó khăn nhất của những người làm việc liên quan đến xây dựng các mô hình toán học phức tạp là giải thích kết quả của mình cho những người ít có kiến thức chuyên môn về lĩnh vực này. Kinh nghiệm của chúng tôi là hãy trực quan hóa kết quả của mình thay vì các công thức phức tạp. Dưới đây là một vài khái niệm toán học phức tạp được giải thích dưới dạng đồ thị động

  1. Chuyển động Brown: chuyển động Brown là một quá trình ngẫu nhiên có ý nghĩa đặc biệt quan trọng trong tài chính, bảo hiểm, và cả các lĩnh vực công nghệ. Không dễ dàng để giải thích cho những người không có nền tảng về toán các khái niệm về chuyển động Brown. Thay vì các công thức toán, chúng ta có thể giải thích về chuyển động Brown thông qua trực quan hóa:

  2. Markov Chain Monte Carlo là một kỹ thuật mô phỏng biến ngẫu nhiên hoặc một véc-tơ ngẫu nhiên có hàm phân phối \(F\) mà không thể mô phỏng được một cách trực tiếp. Quá trình tạo ra biến ngẫu nhiên có hàm phân phối \(F\) sẽ bắt đầu từ một phân phối \(G\) mà chúng ta có thể mô phỏng ra được đi qua các hàm phân phối trung gian và sẽ hội tụ đến phân phối \(F\). Hình vẽ dưới đây mô tả quá trình mô phỏng biến ngẫu nhiên phân phối chuẩn \(\mathcal{N}(0,1)\) từ một phân phối có hai đinh (2 mode).

8.9 Kiến thức nâng cao về \(ggplot2\)

8.9.1 Lập trình trong \(ggplot2\)

8.9.2 Tạo dashboard với \(shiny\)

## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
## 
## Attaching package: 'kableExtra'
## The following object is masked from 'package:dplyr':
## 
##     group_rows
## 
## Attaching package: 'gridExtra'
## The following object is masked from 'package:dplyr':
## 
##     combine
colorize <- function(x, color) {
 if (knitr::is_latex_output()) {
 sprintf("\\textcolor{%s}{%s}", color, x)
 } else if (knitr::is_html_output()) {
 sprintf("<span style='color: %s;'>%s</span>", 
 color,
 x)
 } else x
}

9 Mô hình hồi quy tuyến tính

9.1 Mô hình hổi quy tuyến tính đơn

9.2 Mô hình hồi quy tuyến tính đa biến

9.3 Những cân nhắc khi xây dựng mô hình hồi quy tuyến tính

9.4 Mở rộng mô hình hồi quy tuyến tính

9.5 Thực hành: Xây dựng mô hình hổi quy tuyến tính cho dữ liệu Sales

9.6 Thực hành: Mô hình hổi quy tuyến tính mở rộng

## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
## 
## Attaching package: 'kableExtra'
## The following object is masked from 'package:dplyr':
## 
##     group_rows
## 
## Attaching package: 'gridExtra'
## The following object is masked from 'package:dplyr':
## 
##     combine
## Classes and Methods for R developed in the
## Political Science Computational Laboratory
## Department of Political Science
## Stanford University
## Simon Jackman
## hurdle and zeroinfl functions by Achim Zeileis

10 Mô hình tuyến tính tổng quát.

Mô hình tuyến tính tổng quát, Generalized Linear Model hay viết tắt là GLM, được sử dụng rộng rãi trong các doanh nghiệp, các cơ quan tổ chức hoạt động trong lĩnh vực tài chính, ngân hàng, và bảo hiểm. Các chuyên gia quản trị rủi ro trong các ngân hàng sử dụng GLM để chấm điểm tín dụng khách hàng và quyết định phê duyệt tín dụng. Các chuyên gia tính toán thường xuyên sử dụng mô hình GLM để xác định phí thuần của các sản phẩm bảo hiểm, để xác định dự phòng nghiệp vụ, hoặc để phân loại rủi ro mà công ty phải đối mặt. Khái niệm GLM lần đầu tiên được sử dụng trong nghiên cứu của Nelder và Wedderburn (1972) và từ đó đến nay đã có nhiều sách tham khảo tin cậy cho mô hình này như Alan Agresti (2015) hay Annette J. Dobson and Adrian G. Barnett (2018). Đa số các tài liệu tham khảo giới thiệu GLM dưới góc độ toán học và mang nhiều tính lý thuyết. Chương sách này sẽ cố gắng giải thích và tiếp cận GLM từ một góc nhìn mang tính thực hành nhiều hơn. Chúng tôi sẽ không quá đi sâu vào các khía cạnh như giả thiết hay phương pháp ước lượng của mô hình GLM, mà sẽ tập trung vào hướng dẫn bạn đọc ứng dụng GLM trên nhiều kiểu dữ liệu nhất có thể.

Mô hình tuyến tính tổng quát được phát biểu dưới dạng công thức như sau: \[\begin{align} & Y \sim \mathcal{F}_{\boldsymbol{\theta}} \\ & \mathbb{E}(Y|\textbf{X} = \textbf{x}_i) = \mu_i \\ & g(\mu_i) = \beta_0 + \beta_1 \cdot x_{i,1} + \beta_2 \cdot x_{i,2} + \cdots + \beta_p \cdot x_{i,p} \\ & \mu_i = g^{-1}\left(\beta_0 + \beta_1 \cdot x_{i,1} + \beta_2 \cdot x_{i,2} + \cdots + \beta_p \cdot x_{i,p} \right) (\#eq:glm1) \end{align}\] với \(Y\) là biến mục tiêu, \(\mathcal{F}_{\boldsymbol{\theta}}\) là một phân phối xác suất có tham số là một véc-tơ \(\boldsymbol{\theta}\). Giá trị trung bình của biến mục tiêu \(Y\) phụ thuộc vào giá trị của (các) biến độc lập như sau: với điều kiện véc-tơ biến độc lập \(\textbf{X} = (X_1, X_2, \cdots, X_p)\) nhận giá trị \(\textbf{x_i} = (x_{i,1}, x_{i,2}, \cdots, x_{i,p})\), giá trị trung bình của biến phụ thuộc với điều kiện \(\textbf{X} = x_i\), ký hiệu \(Y|\textbf{X} = x_i\) hoặc ngắn gọn hơn là \(Y_i\), được xác định bằng giá trị một hàm số ngược của một hàm số thực \(g\), ký hiệu là hàm \(g^{-1}\), tính tại một tổ hợp tuyến tính của các giá trị \((x_{i,1}, x_{i,2}, \cdots, x_{i,p})\).

Thay vì cố gắng hiểu các khái niệm toán học ở trên, chúng ta hãy thử áp dụng mô hình kể trên trong một trường hợp cụ thể. Chúng ta sẽ xây dựng một mô hình tuyến tính tổng quát mà trong đó biến mục tiêu \(Y\) cho biết người mua bảo hiểm xe ô tô có hay không lựa chọn đầy đủ các quyền lợi bảo hiểm khi ký hợp đồng mua bảo hiểm trách nhiệm dân sự. Dữ liệu được sử dụng có tên là \(MotoInsurance.csv\). Dữ liệu có 6 biến độc lập là:

    1. Độ tuổi của người lái xe, biến \(age\), nhận giá trị là các số nguyên dương từ 15 đến 92 tuổi.
    1. Kinh nghiệm lái xe, biến \(seniority\), cho biết số năm kinh nghiệm lái xe, giá trị là các số nguyên từ 2 đến 40 năm.
    1. Giới tính của người lái xe, biến \(sex\), nhận giá trị “M” nếu người lái xe là nam giới và “F” nếu người lái xe là nữ giới.
    1. Nơi xe được đăng ký, biến \(urban\), nhận giá trị là 1 nếu xe được đăng ký tại khu vực thành phố và nhận giá trị 0 trong các trường hợp còn lại.
    1. Loại hình đăng ký xe, biến \(private\), nhận giá trị là 1 nếu xe mua bảo hiểm là xe đăng ký theo cá nhân và nhận giá trị 0 trong các trường hợp còn lại.
    1. Tình trạng hôn nhân của người lái xe, biến \(marital\), nhận giá trị “C” nếu đã kết hôn, “S” tương ứng với chưa kết hôn, và “O” tương ứng với đã ly dị.

Biến mục tiêu hay biến phụ thuộc là biến \(Y\) nhận một trong hai giá trị, “Yes” nếu người mua bảo hiểm trách nhiệm dân sự đồng ý lựa thêm quyền lợi bảo hiểm bổ sung và “No” nếu người mua bảo hiểm trách nhiệm dân sự không lựa chọn mua quyền lợi bổ sung. Để mô hình ở dạng đơn giản nhất có thể, chúng tôi lựa chọn hai biến độc lập để xây dựng mô hình là biến \(age\) và biến \(sex\). Chúng ta sẽ sử dụng biến \(age\) như một biến kiểu số, trong khi biến \(sex\) là một biến kiểu phân loại/rời rạc nhận một trong hai giá trị là “M” tương ứng với nam giới và “F” tương ứng với nữ giới.

Đoạn lệnh R dưới đây được sử dụng để lấy dữ liệu và phân tích nhanh ảnh hưởng của các biến \(age\)\(sex\) lên quyết định mua bảo hiểm bổ sung của người sở hữu xe ô tô.

dat<-read.csv("../KHDL_KTKD/Dataset/MotoInsurance.csv")

# Đổi các biến Y và sex sang kiểu factor
dat$Y<-as.factor(dat$Y)
dat$sex<-as.factor(dat$sex)

# Thực hiện các phân tích khai phá
p1<-dat%>%ggplot()+geom_boxplot(aes(x = Y, y = age))+
  ggtitle("Mối liên hệ giữa biến age và biến Y")+
  theme_minimal()

p2<-dat%>%ggplot()+geom_bar(aes(x = sex, fill = Y),col = "black")+
  ggtitle("Mối liên hệ giữa biến sex và biến Y")+
  theme_minimal()+
  scale_fill_manual(values = c("white","grey"))

grid.arrange(p1,p2, ncol = 2)

Bạn đọc có thể thấy rằng: đồ thị bên trái cho thấy những người trẻ tuổi hơn có xu hướng đồng ý mua bảo hiểm bổ sung hơn những người nhiều tuổi; đồ thị bên phải cho thấy nữ giới có xu hướng mua bảo hiểm bổ sung cao hơn so với nam giới.

Chúng ta sẽ xây dựng một mô hình tuyến tính tổng quát để xác nhận lại các phân tích ở trên và lượng hóa được ảnh hưởng của các biến \(age\)\(sex\) lên quyết định mua bảo hiểm bổ sung. Hàm số dùng để xây dựng và ước lượng mô hình tuyến tính tổng quát trong R là hàm glm() của thư viện \(stat\):

# Biến Y có phân phối nhị thức
# Hàm g là hàm probit
glm1<-glm(Y ~ age + sex, data=dat, 
          family = binomial(link = "probit")) # khai báo hàm g
summary(glm1)
## 
## Call:
## glm(formula = Y ~ age + sex, family = binomial(link = "probit"), 
##     data = dat)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -1.4495  -0.9278  -0.7382   1.2584   2.0391  
## 
## Coefficients:
##              Estimate Std. Error z value Pr(>|z|)    
## (Intercept)  0.678644   0.079138   8.575   <2e-16 ***
## age         -0.016257   0.001619 -10.041   <2e-16 ***
## sexM        -0.446783   0.047421  -9.422   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 5163.3  on 3999  degrees of freedom
## Residual deviance: 4933.9  on 3997  degrees of freedom
## AIC: 4939.9
## 
## Number of Fisher Scoring iterations: 4

Chúng ta có thể thấy rằng các hệ số của biến \(age\)\(sex\) đều có ý nghĩa thống kê khi các giá trị \(p-value\) đều rất nhỏ. Đúng như chúng nhận định từ phần phân tích khai phá, hệ số tuyến tính của biến \(age\) là số âm, bằng -0.016, cho biết người trẻ tuổi hơn có khả năng đồng ý mua bảo hiểm bổ sung cao hơn. Hệ số ứng với biến giới tính nam là số âm, bằng -0.447, điều này cho biết khả năng nam giới đồng ý mua bảo hiểm bổ sung là thấp hơn so với nữ giới.

Chúng ta có thể viết các thành phần của mô hình tuyến tính tổng quát đã xây dựng ở trên như sau

\[\begin{align} & Y \sim \mathcal{B}(\rho) \\ & \mathbb{E}(Y_i) = \mathbb{E}\left(Y|(age_i, sex_i)\right) = \rho_i \\ & \Phi^{-1}\left(\rho_i\right) = 0.678 - 0.016 \times age_i - 0.447 \times sex_i \\ & \rho_i = \Phi\left(0.678 - 0.016 \times age_i - 0.447 \times sex_i\right) (\#eq:glm2) \end{align}\] trong đó \(\rho_i\) là xác suất hay khả năng người \(i\) mua đầy đủ các quyền lợi của bảo hiểm sau khi đã mua bảo hiểm trách nhiệm dân sự. Hàm \(\Phi\) là hàm phân phối xác suất của biến ngẫu nhiên phân phối chuẩn có trung bình 0 va phương sai bằng 1. Đây là hàm số mà chúng ta lựa chọn để liên kết giữa xác suất mua bảo hiểm bổ sung đến tổ hợp tuyến tính của các biến độc lập.

Từ kết quả của mô hình, chúng ta có thể kết luận rằng khả năng mua bảo hiểm bổ sung phụ thuộc một cách có ý nghĩa thống kê biến độ tuổi (\(age\)) và giới tính (\(sex\)) của người tham gia bảo hiểm trách nhiệm dân sự bắt buộc. Sự phụ thuộc này cụ thể như sau:

  1. Xác suất mà một người mua đầy đủ các quyền lợi bảo hiểm sau khi mua bảo hiểm bắt buộc sẽ GIẢM nếu tuổi của người tham gia bảo hiểm TĂNG, điều này có nghĩa là những người trẻ tuổi hơn thường có nhu cầu mua đầy đủ các quyền lợi bảo hiểm hơn những người lớn tuổi.

  2. Nam giới ít có khả năng mua đầy đủ quyền lợi bảo hiểm như nữ giới.

Mối liên hệ giữa xác suất mua bảo hiểm đầy đủ và các thuộc tính của người được quan sát được mô tả một cách định lượng thông qua phương trình \[\begin{align} \rho_i = \Phi\left(0.678 - 0.016 \times age_i - 0.447 \times sex_i\right) (\#eq:glm3) \end{align}\] trong đó \(age_i\) là tuổi của người tham gia bảo hiểm; \(sex_i\) nhận giá trị bằng 1 nếu người đó là nam giới và 0 nếu người đó là nữ giới; và \(\Phi\) là hàm phân phối xác suất của biến ngẫu nhiên phân phối chuẩn \(\mathcal{N}(0,1)\); miền giá trị của hàm số này đảm bảo cho giá trị xác suất \(\rho_i\) được tính ra nằm trong khoảng (0,1).

Bạn đọc có thể nhận thấy được sự khác biệt giữa mô hình tuyến tính tổng quát ở trên với mô hình tuyến tính thông thường ở hai điểm:

    1. Phân phối xác suất của biến mục tiêu \(Y\) là phân phối nhị thức chứ không phải là phân phối chuẩn.
    1. Mối liên kết giữa giá trị trung bình của biến mục tiêu \(Y\) và tổ hợp tuyến tính của các biến độc lập được thể hiện thông qua một hàm số, trong trường hợp này là hàm \(\Phi\). Trong mô hình tuyến tính thông thường, giá trị trunh bình của biến mục tiêu được mô tả trực tiếp bằng tổ hợp tuyến tính của các biến độc lập.

Hải điểm nêu trên cũng chính là hai cải tiến quan trọng của mô hình tuyến tính tổng so với mô hình hồi quy tuyến tính thông thường. Việc tổng quát hóa phân phối của biến phụ thuộc và thiết lập một hàm liên kết giữa giá trị trung bình của biến phụ thuộc và với các biến độc lập giúp cho mô hình tuyến tính tổng quát linh hoạt hơn rất nhiều khi làm việc với các dữ liệu cụ thể và vẫn giữ được khả năng suy diễn giống như mô hình hồi quy tuyến tính thông thường. Trong phần tiếp theo của chương chúng ta sẽ thảo luận kỹ hơn về các vấn đề này.

10.1 Các nhược điểm của mô hình hồi quy tuyến tính.

Mô hình hồi quy tuyến tính là nền tảng quan trọng cho hầu hết các mô hình học máy và các mô hình trí tuệ nhân tạo hiện tại. Trước khi những mô hình học máy được nghiên cứu và phát triển mạnh mẽ như hiện nay, những người xây dựng mô hình luôn gặp khó khăn khi sử dụng mô hình hồi quy tuyến tính trong nhiều hoàn cảnh. Nguyên nhân là do giả thiết về phân phối xác suất của biến mục tiêu và miền giá trị trung bình của biến mục tiêu của mô hình hồi quy tuyến tính thông thường là không phù hợp với đa số dữ liệu thực tế.

Thật vậy, mô hình hồi quy tuyến tính được thảo luận trong phần trước của cuốn sách có thể được tóm tắt như sau: người xây dựng mô hình cố gắng nghiên cứu mối quan hệ giữa một biến mục tiêu \(Y\) với véc-tơ biến độc lập \(\textbf{X} = (X_1, X_2, \cdots, X_p)\) bằng cách cho rằng mối liên hệ giữa \(Y\)\(\textbf{X}\) là một hàm tuyến tính. Mối liên hệ đó không đồng nhất nên sai số sẽ tồn tại và những người xây dựng mô hình cho rằng sai số có phân phối chuẩn với trung bình bằng 0 và độ lệch chuẩn là một hằng số \(\sigma > 0\). Chúng ta biểu diễn mô hình tuyến tính thông thương như sau

\[\begin{align} & Y = \beta_0 + \beta_1 \cdot X_1 + \beta_2 \cdot X_2 + \cdots + \beta_p \cdot X_p + \epsilon \\ & \epsilon \sim \mathcal{N}(0, \sigma^2) (\#eq:glm4) \end{align}\]

Bạn đọc có thể thấy rằng trong mô hình hồi quy tuyến tính, biến phụ thuộc \(Y\) là biến ngẫu nhiên có phân phối chuẩn có phương sai là \(\sigma^2\) và giá trị trung bình phụ thuộc vào véc-tơ biến độc lập \(\textbf{X}_i\). Với điều kiện biến độc lập nhận giá trị là \(\textbf{x}_i = (x_{i,1}, x_{i,2}, \cdots, x_{i,p})\); chúng ta có mô hình hồi quy tuyến tính như sau:

\[\begin{align} & Y_i \sim \mathcal{N}(\mu_i, \sigma^2) \\ & \mu_i = \beta_0 + \beta_1 \cdot x_{i,1} + \beta_2 \cdot x_{i,2} + \cdots + \beta_p \cdot x_{i,p} (\#eq:glm5) \end{align}\]

Ngoài giả thiết về phân phối chuẩn của \(Y\), mô hình hồi quy tuyến tính thông thường còn cho rằng giá trị trung bình của biến ngẫu nhiên \(Y\) với điều kiện các biến độc lập nhận giá trị \(\textbf{x}_i\), ký hiệu \(\mu_i\), là một tổ hợp tuyến tính của các biến độc lập. Khi các biến độc lập nhận các giá trị bất kỳ, miền giá trị của \(\mu_i\) sẽ là (toàn bộ) tập các số thực \(\mathbb{R}\).

Câu hỏi đặt ra là: làm như thế nào để áp dụng mô hình hồi quy tuyến tính trong các trường hợp như sau?

  1. Biến mục tiêu \(Y\) chỉ nhận hai giá trị là 0 hoặc 1. Đây là trường hợp rất thường gặp phải trong nhiều lĩnh vực khi thực hiện phân tích dữ liệu. Có thể kể đến như: khi biến \(Y\) đại diện cho sự kiện một người có hay không tham gia bảo hiểm xã hội; một người có hay không thực hiện rút bảo hiểm xã hội một lần trong thời gian một khoảng thời gian; một khách hàng có hay không gửi yêu cầu thanh toán bảo hiểm; hay tương tự như ví dụ trong phần đầu của cuốn sách, một khách hàng đã mua bảo hiểm bắt buộc có hay không mua thêm các quyền lợi bảo hiểm bổ sung. Ngoài lĩnh vực bảo hiểm, biến mục tiêu \(Y\) chỉ nhận giá trị 0 hoặc 1 còn xuất hiện trong lĩnh vực ngân hàng tài chính: biến mục tiêu cho biết một giao dịch trực tuyến có phải là một giao dịch gian lận hay không; một khách hàng có hay không tiếp nhận sản phẩm dịch vụ tài chính với mới; một khách hàng có hay không hoàn trả khoản nợ thẻ tín dụng trong thời gian tới,… Trong tất cả các trường hợp kể trên không thể giả thiết phân phối xác suất của biến mục tiêu là phân phối chuẩn. Hơn thế nữa, giá trị trung bình của biến mục tiêu sẽ luôn nằm trong khoảng 0 đến 1, chứ không phải toàn bộ trục số thực. Nếu sử dụng một tổ hợp tuyến tính của các biến độc lập để tính toán giá trị trung bình của biến mục tiêu, chúng ta sẽ có thể gặp các giá trị nhỏ hơn 0 hoặc các giá trị lớn hơn 1.

  2. Biến mục tiêu \(Y\) là biến dạng đếm. Chẳng hạn như \(Y\) cho biết một người tham gia bảo hiểm xã hội gửi yêu cầu bồi thường bao nhiêu lần trong một năm; biến \(Y\) cho biết một khách hàng mua bảo hiểm xe ô tô gây ra bao nhiêu tai nạn trong thời gian được bảo hiểm,… Trong trường hợp này, \(Y\) sẽ nhận giá trị kiểu số đếm: \(0, 1, 2, \cdots\) tương ứng với số lần khách hàng gửi yêu cầu bảo hiểm. Không thể sử dụng mô hình hồi quy tuyến tính thông thường do biến \(Y\) là biến rời rạc đồng thời giá trị trung bình của \(Y\) là một số lớn hơn 0.

  3. Ngay cả khi trong các trường hợp biến phụ thuộc \(Y\) là biến liên tục, sử dụng mô hình tuyến tính thông thường cũng sẽ gặp phải vấn đề. Chẳng hạn như khi \(Y\) là số tiền khách hàng yêu cầu bồi thường cho xe ô tô trong trường hợp xảy ra tai nạn. Biến \(Y\) chỉ nhận giá trị là số dương và thường có phân phối xác suất lệch phải với đuôi lớn. Sử dụng giả thiết phân phối chuẩn cho biến \(Y\) sẽ làm cho mô hình không đánh giá đúng khả mức độ nghiêm trọng của yêu cầu bồi thường do phân phối chuẩn không có khả năng mô tả các rủi ro có đuôi lớn. Đồng thời, giá trị trung bình của số tiền yêu cầu bồi thường luôn là số dương, do đó cũng không thể sử dụng tổ hợp tuyến tính của các biến độc lập để trực tiếp mô tả giá trị trung bình của \(Y\).

Có thể tổng kết rằng hai vấn đề thường gặp phải khi sử dụng mô hình hồi quy tuyến tính thông thường trên dữ liệu thực tế là

  • Thứ nhất: sự không phù hợp của giả thiết phân phối chuẩn đối với biến mục tiêu \(Y\); và

  • Thứ hai: miền giá trị trung bình của biến mục tiêu \(Y\) không phù hợp với miền giá trị của tổ hợp tuyến tính của biến độc lập. Giá trị \(\textbf{x}_i^{'} \boldsymbol{\beta} = \beta_0 + \beta_1 \ x_{i,1} + \beta_2 \ x_{i,2} + \cdots + \beta_p \ x_{i,p}\) có thể nhận bất kỳ giá trị nào trong \(\mathbb{R}\), trong khi giá trị trung bình của biến mục tiêu \(Y\) trong các dữ liệu thực tế lại thường chỉ là một tập con của \(\mathbb{R}\).

Mô hình tuyến tính tổng quát được xây dựng trên cơ sở của mô hình hồi quy tuyến tính thông thường với mục đích khắc phục hai nhược điểm kể trên. - Trước hết, mô hình tuyến tính tổng quát giả thiết một phân phối phù hợp cho biến phụ thuộc \(Y\), tùy vào dữ liệu sử dụng để phân tích, tạm gọi là phân phối \(F\) với tham số \(\boldsymbol{\theta}\), ký hiệu là \(F_\boldsymbol{\theta}\). - Tiếp theo, để đảm bảo miền giá trị của giá trị trung bình của \(Y\) với điều kiện các biến độc lập bằng \(\textbf{x}_i\), \(\mu_i = E(Y|\textbf{X} = \textbf{x}_i)\), phù hợp với miền giá trị của \(\textbf{x}_i^{'} \boldsymbol{\beta}\), mô hình tuyển tính tổng quát sử dụng một hàm số đơn điệu \(g\), được gọi là hàm liên kết, để biến đổi miền giá trị của \(\mu_i\). Chúng ta phát biểu mô hình tuyến tính tổng quát như sau \[\begin{align} & Y \sim F_\theta \\ & \mathbb{E}(Y|\textbf{X} = \textbf{x}_i) = \mu_i \\ & g(\mu_i) = \beta_0 + \beta_1 \cdot x_{i,1} + \beta_2 \cdot x_{i,2} + \cdots + \beta_p \cdot x_{i,p} (\#eq:glm6) \end{align}\]

Trước hết, có thể thấy rằng mô hình hồi quy tuyến tính thông thường là trường hợp đặc biệt của mô hình tuyến tính tổng quát khi tham số \(\boldsymbol{\theta}\)\(\sigma^2\); phân phối \(F\) là phân phối chuẩn; và hàm \(g\) là hàm số đồng nhất \(g(x) = x\).

Giả thiết đơn điệu của hàm liên kết \(g\) đảm bảo sự tồn tại của hàm số ngược \(g^{-1}\). Mối liên hệ của \(\mu_i\)\(\textbf{x}_i^{'} \boldsymbol{\beta}\) có thể được viết lại dưới dạng hàm ngược của hàm liên kết như sau \[\begin{align} \mu_i = g^{-1}(\textbf{x}_i^{'} \boldsymbol{\beta}) (\#eq:glm7) \end{align}\]

Quay trở lại ví dụ ở phần trước của chương sách, chúng ta phân tích về tác động của độ tuổi và giới tính lên quyết định có tham gia quyền lợi bảo hiểm bổ sung hay không. Mô hình tuyến tính tổng quát được xây dựng như sau:

\[\begin{align} & Y \sim \mathcal{B}(p) \\ & \mathbb{E}(Y|age_i, sex_i) = p_i \\ & \Phi^{-1}(p_i) = 0.678 - 0.016 \times age_i - 0.446 \times sex_i \end{align}\]

Phân phối xác suất của biến \(Y\) là phân phối nhị thức do \(Y\) chỉ có thể nhận là 0 hoặc 1. Đồng thời, giá trị trung bình của \(Y\) nằm trong khoảng \((0,1)\) chúng ta có thể chọn hàm liên kết \(g\) là hàm \(\Phi^{-1}\). Đây là hàm ngược của hàm phân phối xác suất của biến ngẫu nhiên phân phối chuẩn \(\mathcal{N}(0,1)\) nên thỏa mãn các điều kiện của một hàm liên kết: hàm đơn điệu tăng; có miền xác định là \((0,1)\); và miền giá trị là tập số thực \(\mathbb{R}\). Nếu không lựa chọn \(\Phi^{-1}\), mọi hàm số đơn điệu, có miền xác định là khoảng \((0,1)\), và miền giá trị là tập số thực \(\mathbb{R}\) đều có thể được lựa chọn làm hàm số kết nối.

10.2 Xây dựng mô hình tuyến tính tổng quát

Phần tiếp theo của cuốn sách sẽ thảo luận về cách xây dựng mô hình tuyến tổng quát với các kiểu giá trị khác nhau của biến phụ thuộc \(Y\). Xin được nhắc lại rằng đây là cuốn sách dành cho cả các bạn đọc không có nền tảng toán học nâng cao. Do đó, những thảo luận phức tạp liên quan đến các giả thiết của mô hình hay phương pháp ước lượng tham số sẽ được trình bày ở phần sau của chương sách. Chúng ta sẽ hiểu về mô hình tuyến tính tổng quát thông qua việc ứng dụng mô hình cho các các dữ liệu thực tế trước khi đi sâu vào bản chất toán học của mô hình.

10.2.1 Biến phụ thuộc là biến dạng nhị phân.

Đây là các trường hợp mà biến phụ thuộc chỉ nhận một trong hai giá trị. Ở phần trên chúng ta đã trình bày các ví dụ cho trường hợp này: khách hàng có hay không hoàn trả nợ thẻ tín dụng, khách hàng phản hồi tích cực hay tiêu cực về sản phầm, một yêu cầu bồi thường là trục lợi hay bình thường, một giao dịch rút tiền ngân hàng có hay không phải là giao dịch gian lận, … Mặc dù đây chỉ là một trường hợp đặc biệt của biến phụ thuộc nhận giá trị rời rạc nhưng qua các ví dụ thực tế lại thấy rằng phần lớn các dữ liệu gặp phải lại có biến phụ thuộc ở dạng nhị phân. Khi \(Y\) chỉ nhận hai giá trị, chúng ta sẽ luôn mã hóa giá trị của \(Y\) thành hai số là 0 và 1. Một vài cuốn sách khác, hoặc trong một vài lĩnh vực nghiên cứu khác, người xây dựng mô hình có thể mã hóa \(Y\) thành -1 và 1. Tuy nhiên hai cách mã hóa này chỉ khác nhau ở hình thức chứ không làm ảnh hưởng đến kết quả của mô hình.

10.2.1.1 Phân phối xác suất của biến phụ thuộc.

Khi biến phụ thuộc là biến dạng nhị phân, một cách tự nhiên, chúng ta sẽ sử dụng phân phối nhị thức, hay còn gọi là phân phối Bernoulli, để mô tả biến phụ thuộc. Biến ngẫu nhiên \(B\) có phân phối nhị thức với tham số \(\rho\), \(0 < \rho < 1\), ký hiệu \(\mathcal{B}(\rho)\), là biến ngẫu nhiên chỉ nhận hai giá trị là 0 và 1 với hàm khối lượng xác suất như sau
\[\begin{align} \mathbb{P}(B = x) = \rho^x \times (1-\rho)^{(1-x)} \text{ với } x \in \{0;1\} (\#eq:glm80) \end{align}\]

Chúng ta có giá trị trung bình và phương sai của \(\mathcal{B}(\rho)\). \[\begin{align} & \mathbb{E}(B) = \rho \\ & \mathbb{V}(B) = \rho \ (1-\rho) (\#eq:glm81) \end{align}\]

Có thể thấy rằng phân phối nhị thức có duy nhất một tham số \(\rho\) và tham số này cũng chính là giá trị trung bình của biến đó. Phương sai của \(\mathcal{B}(\rho)\) nhỏ hơn giá trị trung bình.

10.2.1.2 Lựa chọn hàm liên kết.

Khi biến phụ thuộc \(Y\) có phân phối nhị thức, giá trị trung bình của biến phụ thuộc sẽ nằm trong khoảng \((0,1)\). Từ công thức @ref(eq:glm6), nếu cho \(\rho_i\) là giá trị trung bình của biến phụ thuộc với điều kiện các biến độc lập \(\textbf{X} = \textbf{x}_i\), ta có

\[\begin{align} & \mathbb{E}(Y|\textbf{X} = \textbf{x}_i) = \rho_i \\ & g(\rho_i) = \beta_0 + \beta_1 \cdot x_{i,1} + \beta_2 \cdot x_{i,2} + \cdots + \beta_p \cdot x_{i,3} (\#eq:bn1) \end{align}\]

Như vậy, mọi hàm số đơn điệu có miền xác định là khoảng \((0,1)\) và miền giá trị là toàn bộ tập số thực \(\mathbb{R}\) đều có thể được lựa chọn để làm hàm ngược của hàm liên kết. Làm thế nào để có được những hàm số có tính chất như vậy? Chúng ta đều biết rằng các hàm phân phối xác suất của một biến ngẫu nhiên liên tục bất kỳ là các hàm số tăng, có miền xác định là \(\mathbb{R}\) và miền giá trị là khoảng \((0,1)\). Do đó, hàm số ngược của các hàm phân phối xác suất, sẽ là các hàm số tăng, có miền xác định là khoảng \((0,1)\) và miền giá trị là \(\mathbb{R}\). Nói một cách khác, hàm số ngược của các hàm phân phối xác suất bất kỳ thỏa mãn đầy đủ tính chất của hàm liên kết trong trường hợp \(Y\) có phân phối nhị thức.

Trong thực tế, việc lựa chọn hàm liên kết còn có mục tiêu là để mô hình dễ giải thích và quá trình ước lượng mô hình đơn giản nhất có thể. Các hàm phân phối xác suất thường được lựa chọn làm \(g^{-1}(.)\) bao gồm có: thứ nhất là hàm phân phối của biến ngẫu nhiên logistic; thứ hai là hàm phân phối của biến ngẫu nhiên phân phối chuẩn; và thứ ba là hàm phân phối của biến ngẫu nhiên phân phối Cauchy.

  1. Hàm phân phối của biến ngẫu nhiên logistic và hàm ngược được cho bởi công thức sau \[\begin{align} & \textit{Hàm phân phối xác suất: } \ g^{-1}(x) = \cfrac{1}{1 + e^{-x}} \\ & \textit{Hàm ngược: } \ g(x) = ln(\cfrac{x}{1- x}) \end{align}\]

Với biến phụ thuộc \(Y\) là có phân phối nhị thức và hàm \(g\) là hàm số ngược của hàm phân phối của biến ngẫu nhiên logistic, chúng ta có mô hình tuyến tính tổng quát như sau:

\[\begin{align} & Y_i \sim \mathcal{B}(\rho_i) \\ & \rho_i = \cfrac{1}{1 + exp(-(\beta_0 + \beta_1 \cdot x_{i,1} + \beta_2 \cdot x_{i,2} + \cdots + \beta_p \cdot x_{i,p}))} \\ & ln(\cfrac{\rho_i}{1 - \rho_i}) = \beta_0 + \beta_1 \cdot x_{i,1} + \beta_2 \cdot x_{i,2} + \cdots + \beta_p \cdot x_{i,p} (\#eq:logit1) \end{align}\]

Đây là mô hình được biết đến rộng rãi với tên gọi là hồi quy logistic và cũng là mô hình thường được sử dụng nhất trong khi biến phụ thuộc là biến nhị phân. Mô hình có ưu điểm là sự dễ hiểu khi diễn giải kết quả:

  • \(\rho_i\) ngoài ý nghĩa là trung bình của biến ngẫu nhiên \(Y_i\) còn có ý nghĩa là xác suất xảy ra sự kiện \(Y_i = 1\).

  • Giá trị \(\cfrac{\rho_i}{1 - \rho_i}\) được gọi là odds của sự kiện \(Y_i = 1\). Mối liên hệ giữa quan sát thứ \(i\) của biến độc lập \(X_j\)\(x_{i,j}\) và obbs của sự kiện \(Y_i = 1\) có thể được diễn giải thông qua hệ số \(\beta_j\).

  1. Hàm phân phối của biến ngẫu nhiên chuẩn \(\mathcal{N}(0,1)\) và hàm ngược của hàm phân phối được cho bởi công thức sau \[\begin{align} & g^{-1}(x) = \Phi(x) \\ & g(x) = \Phi^{-1}(x) \\ & \Phi(x) = \cfrac{1}{\sqrt{2 \pi}} \ \int\limits_{-\infty}^x \ exp(-t^2/2) \\ \end{align}\]

Chúng ta có mô hình tuyến tính tổng quát như sau

\[\begin{align} & Y_i \sim \mathcal{B}(\rho_i) \\ & \rho_i = \Phi(\beta_0 + \beta_1 \cdot x_{i,1} + \beta_2 \cdot x_{i,2} + \cdots + \beta_p \cdot x_{i,p}) \\ & \Phi^{-1}(\rho_i) = \beta_0 + \beta_1 \cdot x_{i,1} + \beta_2 \cdot x_{i,2} + \cdots + \beta_p \cdot x_{i,p} (\#eq:probit1) \end{align}\]

Hàm số ngược của biến ngẫu nhiên phân phối chuẩn được gọi là hàm probit do đó mô hình GLM trong trường hợp này còn được biết đến với tên gọi là mô hình probit.

  1. Hàm phân phối của biến ngẫu nhiên Cauchy và hàm ngược của hàm phân phối được cho bởi công thức sau \[\begin{align} & g^{-1}(x) = \cfrac{1}{2} + \cfrac{arctan(x)}{\pi} \\ & g(x) = tan\left( \pi \left( x - \cfrac{1}{2} \right) \right) (\#eq:cauchy1) \end{align}\]

Chúng ta có mô hình tuyến tính tổng quát như sau

\[\begin{align} & Y_i \sim \mathcal{B}(\rho_i) \\ & p_i = \cfrac{1}{2} + \cfrac{arctan(\beta_0 + \beta_1 \cdot x_{i,1} + \beta_2 \cdot x_{i,2} + \cdots + \beta_p \cdot x_{i,p})}{\pi} \\ & tan\left( \pi \left( \rho_i - \cfrac{1}{2} \right) \right) = \beta_0 + \beta_1 \cdot x_{i,1} + \beta_2 \cdot x_{i,2} + \cdots + \beta_p \cdot x_{i,p} (\#eq:cauchy2) \end{align}\]

Hàm số ngược của phân phối cauchy còn được gọi là hàm cauchit, do đó mô hình tuyến tính tổng quát trong trường hợp này còn được biết đến với tên là mô hình cauchit.

Hình vẽ dưới đây mô tả hình dạng của ba hàm số kết nối

Mô hình tuyến tính tổng quát trong trường hợp \(Y\) là biến nhị phân thường được ước lượng bằng phương pháp tối đa hóa hàm likelihood, hay gọi tắt là phương pháp MLE. Để tránh sự phức tạp không cần thiết chúng tôi sẽ trình bày phần ước lượng mô hình trong phần sau của chương sách.

Trong R, hàm số glm() của thư viện \(stat\) được sử dụng để xây dựng mô hình tuyến tính tổng quát. Tham số \(family\) trong hàm glm() dùng để khai báo phân phối xác suất cho biến phụ thuộc \(Y\) và để lựa chọn hàm liên kết phù hợp. Trở lại với ví dụ khi \(Y\) là biến nhị phân mô tả khách hàng có hay không lựa chọn các quyền lợi đầy đủ khi tham gia bảo hiểm bổ sung, chúng ra thực hiện xây dựng mô hình như sau

# Phân phối Y là nhị thức và hàm liên kết là hàm logit
glm.binomial.logit<-glm(Y ~ age + sex, data=dat, 
          family = binomial(link = "logit"))

# Phân phối Y là nhị thức và hàm liên kết là hàm probit
glm.binomial.probit<-glm(Y ~ age + sex, data=dat, 
          family = binomial(link = "probit"))

# Phân phối Y là nhị thức và hàm liên kết là hàm cauchit
glm.binomial.cauchy<-glm(Y ~ age + sex, data=dat, 
          family = binomial(link = "cauchit"))

Cả ba mô hình tuyến tính tổng quát ở trên đều cho \(Y\) là một biến ngẫu nhiên phân phối nhị thức, nhưng mối liên hệ giữa độ tuổi và giới tính đến giá trị trung bình của \(Y\) lại được mô tả bằng các công thức khác nhau

  1. Trong mô hình logit: \[\begin{align} \mathbb{P}(Y_i = 1| age_i, sex_i) = \cfrac{1}{1 + exp(-(1.109 - 0.026 \times age_{i} - 0.723 \times sex_{i}))} \end{align}\]

  2. Trong mô hình probit \[\begin{align} \mathbb{P}(Y_i = 1| age_i, sex_i) = \Phi\left(0.678 - 0.016 \times age_{i} - 0.446 \times sex_{i}\right) \end{align}\]

  3. Trong mô hình cauchit \[\begin{align} \mathbb{P}(Y_i = 1| age_i, sex_i) = \cfrac{1}{2} + \cfrac{arctan\left(0.997 - 0.024 \times age_{i} - 0.624 \times sex_{i}\right)} {\pi} \end{align}\]

Cả ba mô hình đều cho cùng một kết quả: người có độ tuổi càng cao thì càng ít có khả năng lựa chọn quyền lợi bảo hiểm bổ sung và xác suất nam giới lựa chọn quyền lợi bảo hiểm bổ sung là ít hơn so với nữ giới. Bảng dưới đây đưa ra khả năng/xác suất chấp nhận của ba mô hình tính dựa trên hai biến độc lập là tuổi và giới tính

Người BH Độ tuổi Giới tính Logit Probit Cauchit
1 20 Nam 0.467 0.465 0.466
2 30 Nam 0.403 0.402 0.394
3 40 Nam 0.342 0.342 0.331
4 50 Nam 0.286 0.285 0.280
5 60 Nam 0.236 0.233 0.240
6 20 Nữ 0.643 0.640 0.652
7 30 Nữ 0.582 0.578 0.586
8 40 Nữ 0.517 0.515 0.512
9 50 Nữ 0.452 0.451 0.436
10 60 Nữ 0.389 0.389 0.367

Có thể thấy rằng không có sự khác biệt lớn trong tính toán xác suất của \(Y\) khi sử dụng hàm liên kết khác nhau. Thực tế thì việc lựa chọn hàm kết nối sẽ không ảnh hưởng lớn đến kết quả của mô hình bằng việc lựa chọn phân phối cho biến phụ thuộc hay việc lựa chọn biến độc lập để giải thích mô hình.

10.2.2 Biến phụ thuộc là biến rời rạc không có thứ tự.

Biến rời rạc không có thứ tự, hay còn gọi là biến rời rạc danh nghĩa (non-ordinal variable), là biến ngẫu nhiên mà các giá trị có thể nhận không có ý nghĩa so sánh với nhau. Khi nói đến biến rời rạc không có thứ tự, chúng ta luôn hiểu rằng biến nhận từ ba giá trị trở lên bởi vì trường hợp có hai giá trị sẽ tương tự như biến có phân phối nhị phân. Một ví dụ đơn giản cho biến rời rạc không có thứ tự là màu sắc được chọn khi mua xe ô tô, hoặc loại hình bảo hiểm được lựa chọn bởi các khách hàng của một công ty bảo hiểm.

Giả sử biến rời rạc danh nghĩa \(Y\) có thể nhận \(J\) giá trị khác nhau lần lượt là \(1, 2, \cdots, J\). Với biến danh nghĩa, chúng ta không thể mô tả được mối liên hệ giữa xác suất của hai sự kiện \((Y=i)\)\((Y=j)\) với \(1 \leq i < j \leq J\) dưới dạng tham số. Chính vì thế cấu trúc dạng tham số của mô hình tuyến tính tổng quát như phương trình @ref(eq:glm1) là không thể áp dụng được.

Một phương pháp tiếp cận cho trường hợp biến mục tiêu \(Y\) là biến danh nghĩa là mở rộng mô hình tuyến tính tổng quát với biến nhị phân: với mỗi \(j \in {1,2,\cdots,J}\)

\[\begin{align} \mathbb{P}\left(Y = j\right|\textbf{x}_i) = \cfrac{h(\beta_{j,0} + \beta_{j,1} \cdot x_{i,1} + \cdots + \beta_{j,p} \cdot x_{i,p} ) }{ \sum\limits_{j=1}^J h(\beta_{j,0} + \beta_{j,1} \cdot x_{i,1} + \cdots + \beta_{j,p} \cdot x_{i,p} ) } (\#eq:sm) \end{align}\]

với hàm \(h\): \(\mathbb{R} \rightarrow \mathbb{R}^+\). Hàm số \(h\) thường được lựa chọn là hàm lũy thừa cơ số tự nhiên exp(). Hàm số xác định xác suất xảy ra các sự kiện \((Y=j)\) trong phương trình @ref(eq:sm) được gọi là hàm \(softmax\): \[\begin{align} softmax(z_1, z_2, \cdots, z_p) = \left(\cfrac{e^{z_1}}{\sum\limits_{j=1}^p e^{z_j}}, \cfrac{e^{z_2}}{\sum\limits_{j=1}^p e^{z_1}}, \cdots, \cfrac{e^{z_p}}{\sum\limits_{i=1}^p e^{z_j}} \right) \end{align}\]

Các tham số \(\beta_{j,k}\) được ước lượng để tối thiểu hóa hàm tổn thất tính bằng cross-entropy: \[\begin{align} \sum\limits_{i=1}^n \sum\limits_{j=1}^J y_{i,j} \cdot \log\left(\mathbb{P}\left(Y = j|\textbf{x}_i\right)\right) \end{align}\]

trong đó \(\mathbb{P}\left(Y = j|\textbf{x}_i\right)\) được tính toán từ công thức @ref(eq:sm) và \(y_{i,j}\) nhận một trong hai giá trị:

  • bằng 1 nếu giá trị quan sát thứ \(i\) của biến mục tiêu \(Y\) bằng \(j\)

  • bằng 0 nếu giá trị quan sát thứ \(i\) của biến mục tiêu \(Y\) khác \(j\)

Lưu ý rằng có \(J\) véc-tơ hệ số tuyến tính trong phương trình @ref(eq:sm). Trong thực tế, khi biến mục tiêu \(Y\) có thể nhận \(J\) giá trị danh nghĩa, chúng ta xây dựng mô hình với \((J-1)\) véc-tơ hệ số tuyến tính tương ứng với các giá trị danh nghĩa \(1, 2, \cdots, (J-1)\), đồng thời cố định giá trị của tất cả các hệ số tuyến tính bằng 0 với giá trị danh nghĩa \(J\). Khi tất cả các hệ số tuyến tính bằng 0, chúng ta có \(h(.) = exp(0) = 1\) với mọi \(\textbf{x}_i\).

\[\begin{align} & \mathbb{P}\left(Y = j\right|x_i) = \cfrac{h(\beta_{j,0} + \beta_{j,1} \cdot x_{i,1} + \cdots + \beta_{j,p} \cdot x_{i,p} ) }{1 + \sum\limits_{j=1}^{J-1} h(\beta_{j,0} + \beta_{j,1} \cdot x_{i,1} + \cdots + \beta_{j,p} \cdot x_{i,p} )} \textit{ với } j < J \\ & \mathbb{P}\left(Y = J\right|x_i) = \cfrac{1}{1 + \sum\limits_{j=1}^{J-1} h(\beta_{j,0} + \beta_{j,1} \cdot x_{i,1} + \cdots + \beta_{j,p} \cdot x_{i,p} )} \\ (\#eq:sm1) \end{align}\]

Chúng ta sẽ xây dựng mô hình phân loại biến mục tiêu trên dữ liệu “travel insurance.csv”. Dữ liệu cung cấp thông tin về các sản phẩm bảo hiểm du lịch từ một đại lý du lịch, với biến mục tiêu là \(product.name\) cho biết khách hàng đã lựa chọn sản phẩm sản phẩm bảo hiểm nào. Biến mục tiêu có bốn giá trị tương ứng với bốn loại sản phẩm, mặc dù các sản phẩm này có mức giá khác nhau nhưng việc so sánh các giá trị là không có ý nghĩa. Dữ liệu có 10 biến độc lập, tuy nhiên, để đơn giản hóa, chúng ta chỉ lấy hai biến là giới tính và độ tuổi của người tham gia bảo hiểm.

Mối liên hệ giữa độ tuổi và giới tính của khách hàng đến sản phẩm khách hàng lựa chọn được mô tả qua hình vẽ dưới đây

Có thể thấy rằng có sự khác biệt về độ tuổi của những người lựa chọn các sản phẩm bảo hiểm: những người trẻ tuổi có xu hướng lựa chọn “Bronze Plan” và “Silver Plan” trong khi những người lớn tuổi có xu hướng lựa chọn “Basic Plan” và “Value Plan”. Có sự khác biệt về tỷ lệ nam và nữ khi lựa chọn sản phẩ bảo hiểm, tỷ lệ nam giới lựa chọn “Basic Plan” và “Value Plan” cao, trong khi nữ giới có xu hướng lựa chọn “Bronze Plan” và “Silver Plan”.

Để xây dựng mô hình tuyến tính tổng quát cho biến phân loại danh nghĩa, chúng ta sử dụng hàm multinom() từ thư viện nnet:

## # weights:  16 (9 variable)
## initial  value 19625.769270 
## iter  10 value 18535.510505
## final  value 18040.676017 
## converged
## Call:
## multinom(formula = Product.Name ~ Gender + Age, data = dat1)
## 
## Coefficients:
##             (Intercept)    GenderM          Age
## Bronze Plan   1.5631988 -0.5589519 -0.039513361
## Silver Plan   0.6512458 -0.4871457 -0.031596816
## Value Plan   -1.0970424  0.3380718  0.002018711
## 
## Std. Errors:
##             (Intercept)    GenderM         Age
## Bronze Plan  0.07047991 0.04302279 0.001653049
## Silver Plan  0.08280363 0.05117056 0.001938850
## Value Plan   0.08264747 0.05092679 0.001676413
## 
## Residual Deviance: 36081.35 
## AIC: 36099.35

Dựa trên kết quả ước lượng, chúng ta có công thức tính xác suất chấp nhận các sản phẩm bảo hiểm du lịch của một người có giới tính là \(g_i\) và độ tuổi \(a_i\) như sau: với sản phẩm “Basic Plan” \[\begin{align} \mathbb{P}(Y = Basic|g_i, a_i) = \cfrac{1}{S_0} \end{align}\] với \[\begin{align} S_0 = 1 + exp(1.512 - 0.558 \cdot g_i - 0.038 \cdot a_i) + exp(0.563 - 0.489 \cdot g_i - 0.030 \cdot a_i) + exp(-3.028 - 0.246 \cdot g_i - 0.039 \cdot a_i) \end{align}\] Với các sản phẩm Bronze Plan, Silver Plan và Value Plan, ta có \[\begin{align} & \mathbb{P}(Y = Bronze|g_i, a_i) = \cfrac{exp(1.512 - 0.558 \cdot g_i - 0.038 \cdot a_i)}{S_0} \\ & \mathbb{P}(Y = Silver|g_i, a_i) = \cfrac{exp(0.563 - 0.489 \cdot g_i - 0.030 \cdot a_i)}{S_0} \\ & \mathbb{P}(Y = Value|g_i, a_i) = \cfrac{exp(-3.028 - 0.246 \cdot g_i - 0.039 \cdot a_i)}{S_0} \end{align}\]

Bảng dưới đây tính toán xác suất chấp nhận các sản phẩm du lịch theo một vài độ tuổi và giới tính
Khách hàng Độ tuổi Giới tính Bronze Plan Silver Plan Value Plan Basic Plan
1 Nữ 10 0.54 0.23 0.06 0.17
2 Nữ 20 0.48 0.22 0.08 0.22
3 Nữ 30 0.41 0.21 0.10 0.28
4 Nữ 40 0.34 0.19 0.13 0.35
5 Nữ 50 0.27 0.16 0.15 0.41
6 Nữ 60 0.21 0.14 0.18 0.47
7 Nam 10 0.44 0.21 0.11 0.24
8 Nam 20 0.37 0.19 0.15 0.30
9 Nam 30 0.30 0.16 0.18 0.36
10 Nam 40 0.23 0.14 0.21 0.42
11 Nam 50 0.18 0.11 0.24 0.47
12 Nam 60 0.13 0.09 0.27 0.51

Kết quả từ mô hình cho thấy chỉ có “Basic Plan” và “Bronze Plan” được chọn nếu chúng ta chỉ sử dụng hai biến độc lập là giới tính và độ tuổi. Khả năng dự đoán của mô hình không cao do mô hình còn quá đơn giản, chúng tôi muốn nhấn mạnh về cách xây dựng mô hình trước khi cố gắng xây dựng một mô hình có khả năng dự báo tốt. Chúng ta sẽ thảo luận về đánh giá hiệu quả của mô hình ở các phần sau của chương sách.

10.2.3 Biến phụ thuộc là biến rời rạc có thứ tự.

Biến rời rạc có thứ tự, còn gọi là ordinal categorical variable, là các biến nhận giá trị rời rạc mà các giá trị rời rạc có thể so sánh được với nhau. Một ví dụ điển hình cho biến rời rạc có thứ tự là số lần mà một người đi khám chữa bệnh sử dụng bảo hiểm y tế hoặc một khách hàng gửi yêu cầu bồi thường đến công ty bảo hiểm. Đây là trường hợp mà chúng ta có thể sử dụng một phân phối xác suất rời rạc có tham số để mô tả biến phụ thuộc \(Y\) giống như mô hình @ref(eq:gml1).

10.2.3.1 Biến phụ thuộc có phân phối Poisson.

Phân phối rời rạc thường được lựa chọn cho biến phụ thuộc rời rạc có thứ tự là phân phối Poisson. Hàm phân phối xác suất của biến ngẫu nhiên \(Y\) có phân phối Poisson với tham số \(\lambda > 0\), ký hiệu \(Y \sim \mathcal{P}(\lambda)\) được cho bởi công thức sau \[\begin{align} \mathbb{P}(Y = y) = e^{-\lambda} \cdot \cfrac{\lambda^y}{y!} \text{ với } y = 0, 1, 2, \cdots \end{align}\]

Phân phối Poisson thường được sử dụng để mô tả số lần một hiện tượng xảy ra trong một khoảng thời gian nhất định. Nguyên nhân là do phân phối Poisson này có mối liên hệ trực tiếp đến các mô hình có thời gian chờ có phân phối kiểu mũ. Thật vậy, nếu thời gian chờ giữa hai sự kiện liên tiếp xảy ra của một hiện tượng nào đó là một biến ngẫu nhiên liên tục có hàm phân phối xác suất kiểu mũ với tham số \(\gamma\) thì số lần hiện tượng đó xảy ra trong một khoảng thời gian từ \(t_1\) đến \(t_2\) sẽ là một biến ngẫu nhiên phân phối Poisson với tham số \(\lambda = \cfrac{(t_1 - t_2)}{\gamma}\). Thật vậy, với \(T\) là khoảng thời gian giữa 2 sự kiện liên tiếp xảy ra và \(T\) có phân phối mũ: \[\begin{align} \mathbb{P}(T \leq x) = 1 - exp(-\gamma x) \end{align}\] và nếu \(N\) là số lần xảy ra sự kiện (đi khám bệnh, lái xe gây ra tai nạn) giữa hai mốc thời gian \(t_1 < t_2\) thì \(N\) sẽ có phân phối Poisson với tham số \(\lambda\): \[\begin{align} & \mathbb{P}(N = k) = e^{-\lambda} \cdot \cfrac{\lambda^k}{k!}\\ & \lambda = \cfrac{(t_1 - t_2)}{\gamma} \end{align}\]

Biến ngẫu nhiên có phân phối Poisson với tham số \(\lambda\), ký hiệu \(\mathcal{P}(\lambda)\), có tính chất là giá trị trung bình và phương sai đều bằng tham số của biến đó là \(\lambda\). Phân phối Poisson nằm trong họ các phân phối mũ nên sẽ rất thuận tiện trong xây dựng và ước lượng mô hình bằng phương pháp hợp lý tối đa. Ngoài ra, bằng cách cho tham số của phân phối Poisson một phân phối xác suất, chúng ta có thể thu được các phân phối rời rạc linh hoạt hơn trong mô tả các biến ngẫu nhiên dạng đếm được.

Khi xây dựng mô hình tuyến tính với biến mục tiêu \(Y\) có phân phối Poisson, giá trị trung bình \(Y\) nhận giá trị dương nên chúng ta cần chọn các hàm liên kết \(g\) có miền xác định là tập các số thực dương \(\mathbb{R}^+\) và miền giá trị là tập số thực \(\mathbb{R}\). Hàm số thường được lựa chọn là hàm \(log\).

Chúng ta có thể viết mô hình tuyến tính tổng quát khi \(Y\) có phân phối Poisson, thường được gọi tắt là hồi quy Poisson, như sau

\[\begin{align} & Y_i \sim \mathcal{P}(\lambda_i) \\ & log(\lambda_i) = \left(\beta_0 + \beta_1 \cdot x_{i,1} + \beta_2 \cdot x_{i,2} + \cdots + \beta_p \cdot x_{i,p}\right) \\ (\#eq:pois1) \end{align}\]

Dữ liệu được sử dụng để mô tả mô hình tuyến tính tổng quát với biến phụ thuộc phân phối Poisson là dữ liệu \(SingaporeAuto.csv\). Dữ liệu được tổng hợp bởi Hiệp hội bảo hiểm Singapore trong năm 1993 mô tả số vụ tai nạn ô tô xảy ra cùng với các đặc điểm của người lái và đặc điểm của xe gây tai nạn. Cũng giống như các phần trước, chúng ta sẽ xây dựng mô hình ở mức độ đơn giản nhất để bạn đọc dễ dàng hình dung. Biến phụ thuộc trong mô hình là biến \(Clm\_Count\) cho biết số vụ tai nạn mà một lái xe gây ra trong vòng một năm, hai biến phụ thuộc bao gồm có:

  • Biến \(PC\) là biến nhận hai giá trị là 0 tương ứng với xe được đăng ký theo công ty và nhận giá trị 1 tương ứng với xe được đăng ký theo cá nhân.

  • Biến \(NCD\), viết tắt của No Claims Discount, cho biết lịch sử gây ra tai nạn của lái xe. Giá trị \(NCD\) càng cao nghĩa là lịch sử người lái xe càng gây ra ít tai nạn.

Chúng ta sử dụng hàm glm() để xây dựng và ước lượng mô hình tuyến tính tổng quát:

dat<-read.csv("../KHDL_KTKD/Dataset/SingaporeAuto.csv")
glm2<-glm(Clm_Count~PC+NCD, data=dat, family = poisson(link = "log"))
summary(glm2)
## 
## Call:
## glm(formula = Clm_Count ~ PC + NCD, family = poisson(link = "log"), 
##     data = dat)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -0.4685  -0.3801  -0.3520  -0.3283   4.1716  
## 
## Coefficients:
##              Estimate Std. Error z value Pr(>|z|)    
## (Intercept) -2.641681   0.073836 -35.778  < 2e-16 ***
## PC           0.431980   0.091840   4.704 2.56e-06 ***
## NCD         -0.013943   0.002618  -5.327 9.99e-08 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for poisson family taken to be 1)
## 
##     Null deviance: 2887.2  on 7482  degrees of freedom
## Residual deviance: 2848.3  on 7480  degrees of freedom
## AIC: 3849.4
## 
## Number of Fisher Scoring iterations: 6

Bạn đọc có thể thấy rằng cả hai biến \(PC\)\(NCD\) đều có tác động đến giá trị trung bình của biến mục tiêu \(Clm\_Count\) do các giá trị p-value đều nhỏ. Mối liên hệ giữa số lượng tai nạn xảy ra và các biến \(PC\)\(NCD\) được mô tả như sau:

\[\begin{align} & Clm\_Count_i \sim \mathcal{P}(\lambda_i) \\ & \log(\lambda_i) = -2.641 + 0.432 \cdot PC_{i} - 0.013 \cdot NCD_{i} \\ & \lambda_i = \exp\left(-2.641 + 0.432 \cdot PC_{i} - 0.013 \cdot NCD_{i} \right) (\#eq:pois2) \end{align}\]

Hệ số của biến \(PC\) là số dương, điều này cho biết các xe đăng ký dưới dạng cá nhân có khả năng gây tai nạn cao hơn so với xe đăng ký dưới hình thức doanh nghiệp. Đồng thời, hệ số của biến \(NCD\) âm cho biết lái xe có lịch sử lái xe tốt, tương ứng với \(NCD\) cao, ít có khả năng gây ra tai nạn hơn lái xe có lịch sử lái xe không tốt, tương ứng với \(NCD\) thấp.

10.2.3.2 Phân phối rời rạc có lạm phát tại giá trị 0.

Biến phụ thuộc dạng đếm chứa một tỷ lệ lớn giá trị bằng 0 là các kiểu biến phụ thuộc thường gặp phải khi làm việc trên dữ liệu trong lĩnh vực bảo hiểm. Tỷ lệ lớn ở đây thường được hiểu là trên 90% giá trị quan sát được. Đa số các phân phối xác suất rời rạc thông thường, bao gồm phân phối Poisson, không mô tả tốt khi biến phụ thuộc \(Y\) trong trường hợp này.

Một giải pháp phổ biến khi làm việc với kiểu dữ liệu như vậy là thay đổi phân phối dạng đếm thông thường để làm tăng tỷ lệ giá trị nhận được tại 0, để thu được các phân phối thường được gọi là các phân phối lạm phát tại 0, hay Zero-inflated variable. Phân phối lạm phát tại 0 là hỗn hợp của hai phân phối xác suất rời rạc, bao gồm một phân phối nhị thức chỉ báo cho trường hợp 0, và một phân phối xác suất dành cho biến đếm thông thường. Hàm phân phối của biến ngẫu nhiên dạng đếm có lạm phát tại 0, ký hiệu \(ZI_Y\), có thể được mô tả như sau \[\begin{align} \mathbb{P}(ZI_Y = y) = \begin{cases} & \omega + (1-\omega) \cdot \mathbb{P}(Y=0) \text{ khi y = 0} \\ & (1-\omega) \cdot \mathbb{P}(Y=y) \text{ khi y > 0} \end{cases} (\#eq:zip1) \end{align}\] trong đó biến ngẫu nhiên \(Y\) tuân theo phân phối số đếm tiêu chuẩn được. Trong trường hợp tham số \(\omega\) bằng 0, phân phối của biến ngẫu nhiên \(ZI_Y\) sẽ tương ứng tương ứng với phân phối của biến Y.

Tất cả các phân phối kiểu đếm đều có thể được sử dụng để tạo ra các biến mới có lạm phát tại 0. Trong các mô hình tuyến tính tổng quát, biến dạng đếm thường được mô tả bằng phân phối cổ điển Poisson. Với việc sử dụng phương trình @ref(eq:zip1), hàm phân phối của biến ngẫu nhiên Poisson có lạm phát tại 0, ký hiệu là \(ZI_\mathcal{P}\) được cho bởi công thức sau:

\[\begin{align} \mathbb{P}(ZI_\mathcal{P} = y) = \begin{cases} & \omega + (1-\omega) \cdot e^{-\lambda} \text{ khi y = 0} \\ & (1-\omega) \cdot e^{-\omega} \cdot \cfrac{\lambda^y}{y!} \text{ khi y > 0} \end{cases} (\#eq:zip2) \end{align}\]

Chúng ta có thể xác định giá trị trung bình và phương sai của phân phối \(ZI_Y\) dựa trên tham số \(\omega\) và giá trị trung bình cũng như phương sai của biến \(Y\) như sau \[\begin{align} & \mathbb{E}(ZI_Y) = (1-\omega) \cdot \mathbb{E}(Y) \\ & \mathbb{V}(ZI_Y) = (1-\omega) \cdot \mathbb{V}(Y) + \omega(1-\omega) \cdot \mathbb{E}(Y)^2 (\#eq:zip3) \end{align}\]

Trong trường hợp \(Y\) có phân phối Poission, chúng ta có giá trị trung bình và phương sai của biến \(ZI_\mathcal{P}\): \[\begin{align} & \mathbb{E}(ZI_\mathcal{P}) = (1-\omega) \cdot \lambda \\ & \mathbb{V}(ZI_\mathcal{P}) = \mathbb{E}(ZI_\mathcal{P}) \cdot (1 + \lambda - \mathbb{E}(ZI_\mathcal{P})) (\#eq:zip4) \end{align}\]

Với giá trị trung bình và phương sai của biến \(ZI_\mathcal{P}\) như phương trình @ref(eq:zip1), chúng ta có thể xây dựng mô hình tuyến tính tổng quát với biến phụ thuộc là \(ZI_\mathcal{P}\) như sau: giá trị trung bình \(\left((1-\omega) \cdot \lambda\right)\) sẽ được giải thích thông qua các biến phụ thuộc trong khi tham số \(\lambda\) sẽ được ước lượng bằng phương pháp hợp lý tối đa.

Biến phụ thuộc phân phối \(ZI_\mathcal{P}\) sẽ hữu ích cho mục đích lập mô hình trong trường hợp dữ liệu quan sát có sự tập trung quá mức tại giá trị 0. Ngoài phân phối Poisson, các phân phối mở rộng từ phân phối Poisson cũng có thể được sử dụng với để tạo ra các phân phối có lạm phát tại 0, chẳng hạn như biến ngẫu nhiên phân phối Poisson - Gamma, Poisson - Inverse gaussian.

Trong các nghiên cứu thực nghiệm, nhiều tác giả đã chứng minh rằng việc áp dụng phân phối có lạm phát tại 0 để mô hình hóa số lượng yêu cầu bồi thường bảo hiểm là phù hợp để mô tả hành vi của người được bảo hiểm. Thật vậy, trong ngành bảo hiểm không phải tất cả các vụ tai nạn đều được báo cáo, công ty bảo hiểm chỉ có thông tin về các yêu cầu bồi thường được báo cáo. Có hai cách để giải thích cách phân phối có lạm phát tại 0 như sau: (1) Một số người được bảo hiểm không gửi yêu cầu bồi thường dù có xảy ra sự kiện bảo hiểm, do họ không có nhận thức được về việc được bảo hiểm, hoặc không có nhu cầu gửi yêu cầu bảo hiểm. (2) Một cách giải thích khác của mô hình lạm phát tại 0 là xem xét xác suất của mỗi vụ tai nạn được báo cáo. Một hành vi thực tế của người được bảo hiểm là nếu họ đã báo cáo vụ tai nạn đầu tiên thì những vụ tai nạn tiếp theo cũng sẽ được báo cáo, còn nếu vụ tai nạn đầu tiên không được báo cáo thì các vụ tai nạn sau sẽ không được báo cáo. Cả hai cách giải thích dựa trên hành vi này đều dẫn đến việc số lượng biến mục tiêu nhận giá trị bằng 0 cao hơn so với số lượng tai nạn thực tế xảy ra.

Dữ liệu \(exposure.csv\) là dữ liệu điển hình cho biến phụ thuộc có lạm phát tại giá trị 0. Dữ liệu được rút gọn chỉ bao gồm hai biến độc lập là tuổi (Age) và giới tính (Gender) của người được bảo hiểm. Biến phụ thuộc là số lần người được bảo hiểm báo cáo tai nạn trong khoảng thời gian một năm. Sự khác nhau giữa mô hình tuyến tính tổng quát với biến phụ thuộc có phân phối Poisson thông thường và mô hình tuyến tính tổng quát với biến phụ thuộc có phân phối Poisson có lạm phát tại 0 được mô tả như sau:
(#tab:unnamed-chunk-13)Khác nhau giữa GLM - Poisson và GLM - ZIP
Thành phần GLM Poisson GLM Poisson Zero-inflated
Phân phối của biến phụ thuộc \(Y_i \sim \mathcal{P}(\lambda_i)\) \(Y_i \sim ZI_\mathcal{P}(\lambda_i, \omega)\)
Trung bình của biến phụ thuộc \(\lambda_i\) \(\lambda_i \cdot (1-\omega)\)
Tham số của biến Poisson \(\lambda_i = exp(\beta_0 + \beta_1 \cdot Age_i + \beta_2 \cdot Gender_i)\) \(\lambda_i = exp(\beta_0 + \beta_1 \cdot Age_i + \beta_2 \cdot Gender_i)\)
Tham số Zero-inflated 0 \(\omega = exp(\alpha_0)\)

Để xây dựng mô hình tuyến tính tổng quát trong đó biến phụ thuộc có phân phối \(ZI_\mathcal{P}\) chúng ta sử dụng hàm số zeroinfl() của thư viện \(pscl\).

dat<-read.csv("../KHDL_KTKD/Dataset/exposure.csv")
# Biến phụ thuộc có phân phối Poisson
glm1<-glm(Claim_Count~Age+Gender,
          family = poisson(link = "log"),
          data=dat)

# Biến phụ thuộc có phân phối Zero inflated poisson
zip.glm<-zeroinfl(Claim_Count~Age+Gender|1,
                  dist = 'poisson',
                  link = "log",
                  data = dat)
summary(glm1)
summary(zip.glm)

Chúng ta có kết quả ước lượng các mô hình

(#tab:unnamed-chunk-15)Kết quả ước lượng GLM - Poisson và GLM - ZIP
Thành phần GLM Poisson GLM Poisson Zero-inflated
Phân phối của biến phụ thuộc \(Y_i \sim \mathcal{P}(\lambda_i)\) \(Y_i \sim ZI_\mathcal{P}(\lambda_i, \theta)\)
Trung bình của biến phụ thuộc \(\lambda_i\) \(\lambda_i \cdot (1-\theta)\)
Tham số của biến Poisson \(\lambda_i = exp(-0.544 - 0.0214 \cdot Age + 0.106 \cdot Gender)\) \(\lambda_i = exp(-0.063 - 0.0215 \cdot Age + 0.104 \cdot Gender)\)
Tham số Zero-inflated 0 \(\theta = exp(-0.971)\)
Giá trị Log Likelyhood -6419 -6353

Có thể thấy rằng mô hình tuyến tính tổng quát với biến phụ thuộc phân phối \(ZI_\mathcal{P}\) có giá trị hàm hợp lý tối đa lớn hơn, điều này cũng có nghĩa là phân phối \(ZI_\mathcal{P}\) phù hợp hơn phân phối Poisson thông thường khi mô tả biến phụ thuộc trong dữ liệu. Dựa vào kết quả ước lượng từ hai mô hình, chúng ta có thể tính toán xác suất không có tai nạn và xác suất để xảy ra một tai nạn theo độ tuổi và giới tính của người tham gia bảo hiểm như sau

Có thể nhận thấy rằng xác suất mà một người được bảo hiểm không để xảy ra tai nạn trong mô hình tuyến tính tổng quát với biến phụ thuộc có phân phối \(ZI_\mathcal{P}\) là luôn cao hơn so với mô hình tuyến tính tổng quát với biến phụ thuộc có phân phối Poisson thông thường. Đồng thời, mô hình tuyến tính tổng quát với biến phụ thuộc có phân phối \(ZI_\mathcal{P}\) cho kết quả xác suất xảy ra đúng một tai nạn thấp hơn so với mô hình có biến phụ thuộc có phân phối Poisson thông thường.

10.2.4 Biến phụ thuộc có phân phối liên tục.

Một giả thiết quan trọng của mô hình tuyến tính tổng quát là biến phụ thuộc nằm trong họ các biến ngẫu nhiên có phân phối kiểu mũ, hay exponential family. Chúng ta sẽ thảo luận kỹ hơn về họ các biến ngẫu nhiên có phân phối kiểu mũ trong phần sau của cuốn sách. Lưu ý rằng họ các biến ngẫu nhiên có phân phối kiểu mũ không tương đồng với khái niệm biến ngẫu nhiên có phân phối mũ. Phân phối mũ chỉ là mộ trường hợp đặc biệt của phân phối kiển mũ. Có nhiều biến ngẫu nhiên liên tục khác có phân phối nằm trong họ các phân phối kiểu mũ. Có thể kể đến như phân phối gamma, phân phối chuẩn, phân phối chuẩn ngược…

Các bước để xây dựng mô hình tuyến tính tổng quát trong trường hợp biến phụ thuộc \(Y\) là biến ngẫu nhiên liên tục hoàn toàn tương tự như cách xây dựng mô hình tuyến tính tổng quát ở trên, bao gồm bước chọn phân phối xác suất cho biến mục tiêu và lựa chọn hàm liên kết phù hợp. Chúng ta sẽ tiếp tục xây dựng mô hình với dữ liệu “exposure.csv” đã đề cập ở các phần trên. Biến mục tiêu không còn là số lần khách hàng gửi yêu cầu bồi thường, mà là số tiền trung bình mỗi lần khách hàng gửi yêu cầu (biến \(Ave\_Amount\)). Các biến giải thích vẫn tiếp tục là giới tính (\(Gender\)) và độ tuổi (\(Age\)) của người được bảo hiểm.

dat<-read.csv("../KHDL_KTKD/Dataset/exposure.csv")
dat<-mutate(dat,Ave_Amount = ifelse(Claim_Count>0,Total_Claim/Claim_Count,0))
dat1<-filter(dat,Claim_Count>0)

Mối liên hệ giữa \(Ave\_Amount\) được thể hiện qua đồ thị dưới đây

dat1$Gender<-ifelse(dat1$Gender==0,"F","M")
p1<-dat1%>%ggplot()+geom_boxplot(aes(Gender,y = Ave_Amount))+
  ylim(0,200)+ggtitle("Số tiền yêu cầu bồi thường trung bình và giới tính")
p2<-dat1%>%ggplot(aes(x=Age,y = Ave_Amount))+geom_point(alpha=0.2)+
  geom_smooth(col="black", size = 1, se = FALSE)+ylim(0,200)+
  ggtitle("Số tiền yêu cầu bồi thường trung bình và độ tuổi")
grid.arrange(p1,p2,ncol=2)

Đồ thị bên trái cho thấy số tiền yêu cầu bồi thường trung bình của nữ là cao hơn nam giới. Phân phối xác suất của số tiền yêu cầu bồi thương trung bình là phân phối liên tục lệch phải, có đuôi bên phải lớn. Đồ thị bên phải cho thấy số tiền yêu cầu bồi thường trung bình có xu hướng tăng theo độ tuổi.

Số tiền bảo hiểm trung bình là số dương nên chúng ta sẽ sử dụng phân phối \(gamma\) với hàm liên kết là hàm log(). Lưu ý rằng phân phối gamma() là phân phối có hai tham số, thường được ký hiệu là \(\alpha\)\(\gamma\), với hàm mật độ xác suất, giá trị trung bình, và phương sai như sau \[\begin{align} & f_Y(y) = \cfrac{\beta^\alpha}{\Gamma(\alpha)} \ y^{\alpha-1} \ e^{-\gamma y} \\ & \mathbb{E}(X) = \cfrac{\alpha}{\gamma} \\ & \mathbb{V}(X) = \cfrac{\alpha}{\gamma^2} \end{align}\]

Khi phân phối \(gamma\) được sử dụng cho biến phụ thuộc, giá trị trung bình được tính bằng \(\alpha/\gamma\), sẽ được mô tả thông qua các biến độc lập trong khi tham số \(\alpha\) sẽ được ước lượng dựa trên hàm hợp lý tối đa:

dat<-read.csv("../KHDL_KTKD/Dataset/exposure.csv")
dat<-mutate(dat,Ave_Amount = ifelse(Claim_Count>0,Total_Claim/Claim_Count,0))
dat1<-filter(dat,Claim_Count>0)
glm3<-glm(Ave_Amount~Age+Gender, family = Gamma(link = "log"),data = dat1)
summary(glm3)
## 
## Call:
## glm(formula = Ave_Amount ~ Age + Gender, family = Gamma(link = "log"), 
##     data = dat1)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -4.3309  -0.9498  -0.3047   0.3360   2.9954  
## 
## Coefficients:
##              Estimate Std. Error t value Pr(>|t|)    
## (Intercept) -0.047535   0.065517  -0.726    0.468    
## Age          0.079089   0.001534  51.563   <2e-16 ***
## Gender      -0.534010   0.041951 -12.730   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for Gamma family taken to be 0.9241118)
## 
##     Null deviance: 4666.5  on 2110  degrees of freedom
## Residual deviance: 2205.7  on 2108  degrees of freedom
## AIC: 15710
## 
## Number of Fisher Scoring iterations: 6

Ngoài các hệ số của các biến độc lập trong mô hình tuyến tính tổng quát, hàm glm() còn cung cấp giá trị ước lượng được cho tham số \(\phi\) (disperson paramter hay tham số phân tán) là \(0.924\). Tham số phân tán được định nghĩa trong họ các phân phối kiểu mũ sẽ được thảo luận trong phần tiếp theo. Đối với phân phối \(gamma\), tham số phân tán được tính bằng \(\phi = 1/\alpha\).

Giá trị số tiền bồi thường trung bình được được mô tả bằng mô hình tuyến tính tổng quát như sau: \[\begin{align} & Y_i \sim Gamma(\mu_i = \alpha_i/\gamma_i , \phi_i = 1/\alpha_i) \\ & \phi_i = 0.924 \\ & log(\mu_i) = -0.0475 + 0.079 \cdot Age_i + 0.534 \cdot Gender_i \end{align}\]

Trước khi đi vào chi tiết các thành phần của mô hình, chúng tôi muốn kết luận rằng mô hình tuyến tính tổng quát có thể được sử dụng để mô hình hóa dữ liệu trong nhiều hoàn cảnh khác nhau. Việc xây dựng mô hình tuyến tính tổng quát luôn được bắt đầu bằng việc lựa chọn phân phối cho biến phụ thuộc và lựa chọn hàm số liên kết. Có các quy tắc chung trong việc lựa chọn phân phối và hàm liên kết giống như chúng tôi đã trình bày ở phần trên. Tuy nhiên để đưa ra được mô hình tuyến tính tổng quát phù hợp cho từng dữ liệu cụ thể, hoặc để có thể mở rộng được mô hình tuyến tính tổng quát trong các kiểu dữ liệu phức tạp hơn, bạn đọc cần am hiểu sâu hơn về các đặc điểm kỹ thuật của các mô hình này. Các kiến thức này sẽ được trình bày trong các phần tiếp theo của chương.

10.3 Các thành phần của mô hình tuyến tính tổng quát.

Như chúng ta đã thảo luận trong phần trước, mô hình tuyến tính tổng quát khắc phục hai giả thiết, cũng là hai nhược điểm của mô hình tuyến tính thông thường, đó là i) biến phụ thuộc có phân phối chuẩn, và ii) giá trị trung bình của biến phụ thuộc bằng một tổ hợp tuyến tính của các biến độc lập. Trong phần này của cuốn sách, chúng tôi sẽ thảo luận kỹ hơn vào cách tiếp cận để khắc phục các hạn chế kể trên.

  • Thứ nhất, thay vì sử dụng phân phối chuẩn, mô hình tuyến tính tổng quát giả thiết rằng biến phụ thuộc có phân phối nằm trong họ các phân phối kiểu mũ (exponential family). Phân phối chuẩn chỉ là một trường hợp đặc biệt của họ các phân phối này.

  • Thứ hai, mô hình tuyến tính tổng quát sử dụng một hàm \(g\), được gọi là hàm liên kết, để mô tả mối liên hệ giữa giá trị trung bình của biến phụ thuộc với tổ hợp tuyến tính của các biến độc lập. Lựa chọn hàm \(g\) có ý nghĩa quan trọng không chỉ trong cách luận giải kết quả của mô hình, mà còn ở việc dễ dàng ước lượng tham số của mô hình.

10.3.1 Họ các biến ngẫu nhiên có phân phối kiểu mũ.

Thay vì giả thiết rằng \(Y\) có phân phối chuẩn trong mô hình tuyến tính thông thường, mô hình tuyến tính tổng quát giả thiết biến phụ thuộc \(Y\) nằm trong họ các biến ngẫu nhiên có phân phối kiểu mũ. Họ các biến ngẫu nhiên có phân phối kiểu mũ (exponential family) có hàm mật độ xác suất có thể viết dưới dạng như sau: \[\begin{align} f(y;\theta,\phi) = \exp\left[ \cfrac{y \theta - b(\theta)}{a(\phi)} + c(y,\phi) \right] \end{align}\] trong đó

  • Tham số \(\theta\) được gọi là tham số \(chính\) \(tắc\) của phân phối kiểu mũ.
  • Tham số \(\phi\) được gọi là tham số \(phân\) \(tán\). Nguyên nhân là do giá trị trung bình của biến ngẫu nhiên \(Y\) không phụ thuộc vào \(\phi\). Tham số \(\phi\), mà tổng quát hơn là hàm \(a(\phi)\) sẽ xác định phương sai của biến phụ thuộc.
  • Các hàm số \(b(\theta)\), \(a(\phi)\)\(c(y,\phi)\) sẽ quyết định kiểu phân phối của biến phụ thuộc.

Giá trị trung bình và phương sai của biến phụ thuộc \(Y\) được cho bởi các công thức sau: \[\begin{align} & \mathbb{E}(Y) = b^{'}(\theta) \\ & \mathbb{V}(Y) = a(\phi) \cdot b^{''}(\theta) (\#eq:EF1) \end{align}\] với \(b^{'}(\theta)\)\(b^{''}(\theta)\) lần lượt là đạo hàm bậc một và đạo hàm bậc hai của hàm số \(b\) theo biến \(\theta\).

Họ các biến ngẫu nhiên có phân phối kiểu mũ bao gồm đa số các biến ngẫu nhiên liên tục thông thường như phân phối chuẩn, phân phối mũ, phân phối Gamma, phân phối chuẩn ngược. Các biến ngẫu nhiên phân phối rời rạc như phân phối nhị thức, phân phối binomial, hoặc phân phối Poisson cũng nằm trong họ các biến ngẫu nhiên có phân phối kiểu mũ.

  • Ví dụ 1: phân phối Poisson thường được sử dụng để mô tả phân phối của biến đếm trong mô hình tuyến tính tổng quát. Hàm phân phối của biến ngẫu nhiên Poisson với tham số \(\lambda\), ký hiệu \(\mathcal{P}(\lambda)\), được cho bởi công thức \[\begin{align} \mathbb{P}(Y = y; \lambda) = exp(-\lambda) \ \cfrac{\lambda^y}{y!} \end{align}\] Chúng ta có thể viết phân phối \(\mathcal{P}(\lambda)\) dưới dạng phân phối mũ như sau \[\begin{align} \mathbb{P}(Y = y; \theta) = exp\left[ \cfrac{\theta y - exp(\theta)}{1} - log(\Gamma(y+1)) \right] \text{ với } \lambda = exp(\theta) \end{align}\] Đây là hàm phân phối của biến ngẫu nhiên nằm trong họ các phân phối kiểu mũ với \(a(\phi) = 1\); \(b(\theta) = exp(\theta)\)\(c(y,\phi) = log\left(\Gamma(y+1)\right)\). Bạn đọc có thể tính toán trung bình và phương sai của phân phối \(\lambda\) dựa theo công thức @ref(eq:EF1) \[\begin{align} & \mathbb{E}(Y) = b^{'}(\theta) = exp(\theta) = \lambda \\ & \mathbb{V}(Y) = a(\phi) \cdot b^{''}(\theta) = 1 \cdot exp(\theta) = \lambda \end{align}\]

  • Ví dụ 2: phân phối chuẩn là một biến ngẫu nhiên nằm trong họ các phân phối kiểu mũ. Thật vậy, chúng ta có hàm mật độ của biến ngẫu nhiên phân phối chuẩn với trung bình \(\mu\) và độ lệch chuẩn \(\sigma\), ký hiệu \(\mathcal{N}(\mu,\sigma)\), như sau \[\begin{align} f(y,\mu,\sigma) = \cfrac{1}{\sqrt{2 \pi \sigma^2}} \exp\left[ \cfrac{-(y - \mu)^2}{2 \ \sigma^2} \right] \end{align}\] Hàm phân phối của \(\mathcal{N}(\mu,\sigma)\) có thể được viết dưới dạng phân phối kiểu mũ \[\begin{align} \cfrac{1}{\sqrt{2 \pi} \sigma} \exp\left[ \cfrac{-(y - \mu)^2}{2 \ \sigma^2} \right] &= \exp\left[ \cfrac{-(y - \mu)^2}{2 \ \sigma^2} - \cfrac{1}{2} log(2 \pi \sigma^2) \right] \\ &= \exp\left[ \cfrac{\mu y - \mu^2/2} {\sigma^2} - \cfrac{y^2}{2\sigma^2} - \cfrac{1}{2} log(2 \pi \sigma^2) \right] \end{align}\] Với \(\theta = \mu\)\(\phi = \sigma^2\) chúng ta có hàm mật độ của biến ngẫu nhiên \(\mathcal{N}(\mu,\sigma)\) là hàm mật độ của biến ngẫu nhiên nằm trong họ các phân phối kiểu mũ, với \(a(\phi) = \phi\), \(b(\theta) = \theta^2/2\)\[\begin{align} c(y,\phi) = -\cfrac{y^2}{2\phi} - \cfrac{1}{2} log(2 \pi \phi) \end{align}\] Bạn đọc có thể kiểm tra giá trị trung bình và phương sai của phân phối \(\mathcal{N}(\mu,\sigma)\) dựa theo công thức @ref(eq:EF1) \[\begin{align} & \mathbb{E}(Y) = b^{'}(\theta) = \theta = \mu \\ & \mathbb{V}(Y) = a(\phi) \cdot b^{''}(\theta) = \phi \cdot 1 = \phi = \sigma^2 \end{align}\]

  • Ví dụ 3: phân phối \(Gamma(\alpha,\gamma)\) nằm trong họ các phân phối kiểu mũ. Thật vậy \[\begin{align} f(y) &= \cfrac{\gamma^\alpha}{\Gamma(\alpha)} \ y^{\alpha-1} \ e^{-\gamma \cdot y} \\ &= \exp\left[ -\gamma \cdot y + \alpha \log(\gamma) - \log(\Gamma(\alpha)) - (\alpha-1) \cdot \log(y) \right] \end{align}\] Với \(\theta = -\gamma/\alpha\)\(\phi = 1/\alpha\) ta có \[\begin{align} f(y) & = \exp\left[ \cfrac{\theta y + \log(-\theta)}{1/\alpha} - \cfrac{\log(\alpha)}{1/\alpha} - \log(\Gamma(\alpha)) - (\alpha-1) \cdot \log(y) \right] \\ & = \exp\left[ \cfrac{\theta y + \log(-\theta)}{\phi} + \cfrac{\log(\phi)}{\phi} - \log(\Gamma(1/\phi)) - (1/\phi-1) \cdot \log(y) \right] \end{align}\] Chúng ta có phân phối kiểu mũ với \(a(\phi) = \phi\), \(b(\theta) = \log(-\theta)\), và \[\begin{align} c(y,\phi) = \cfrac{\log(\phi)}{\phi} - \log(\Gamma(1/\phi)) - (1/\phi-1) \cdot \log(y) \end{align}\] Chúng ta kiểm tra giá trị trung bình và phương sai của phân phối \(Gamma(\alpha,\beta)\) theo công thức @ref(eq:EF1) \[\begin{align} & \mathbb{E}(Y) = b^{'}(\theta) = - \cfrac{1}{\theta} = \cfrac{\alpha}{\gamma} \\ & \mathbb{V}(Y) = a(\phi) \cdot b^{''}(\theta) = \cfrac{1}{\alpha} \cdot \cfrac{\alpha^2}{\gamma^2} = \cfrac{\alpha}{\gamma^2} \end{align}\]

Trong trường hợp hàm số \(a(\phi)\) là hàm số tuyến tính theo \(\phi\), nghĩa là tồn tại số \(\omega\) sao cho \(a(\phi) = \cfrac{\phi}{\omega}\) chúng ta có hàm mật độ của biến ngẫu nhiên có phân phối kiểu mũ như sau \[\begin{align} f(y;\theta,\phi) = \exp\left[ \cfrac{y \theta - b(\theta)}{\phi/\omega} + c(y,\phi) \right] \end{align}\] Giá trị trung bình và phương sai của biến ngẫu nhiên \(Y\) trong trường hợp này được xác định như sau: \[\begin{align} & \mathbb{E}(Y) = b^{'}(\theta) \\ & \mathbb{V}(Y) = \cfrac{\phi}{\omega} \cdot b^{''}(\theta) \end{align}\]

Giả sử hàm \(b^{'}(.)\) là hàm số đơn điệu và tồn tại hàm số ngược \({b^{'}}^{-1}(.)\) thì \(\theta = {b^{'}}^{-1}\left(\mathbb{E}(Y)\right)\). Do đó mối liên hệ giữa phương sai của biến ngẫu nhiên \(Y\) và giá trị trung bình của biến \(Y\) được thể hiện qua công thức sau \[\begin{align} \mathbb{V}(Y) = \cfrac{\phi}{\omega} \cdot b^{''}({b^{'}}^{-1}(\mathbb{E}(Y))) \end{align}\]

Hàm số \(V(\cdot) = b^{''}({b^{'}}^{-1}(\cdot))\) được gọi là hàm phương sai của phân phối kiểu mũ. Tại sao chúng ta cần định nghĩa một hàm số phức tạp như vậy? Bởi vì hàm \(V(\cdot)\) là cơ sở để người xây dựng mô hình kiểm soát phương sai của biến ngẫu nhiên kiểu mũ.

Giả sử biến phụ thuộc quan sát được là \(Y_1, Y_2, \cdots, Y_n\) có cùng phân phối kiểu mũ với cùng tham số \(\phi\), cùng hàm \(b(\cdot)\)\(c(\cdot)\), nhưng có giá trị \(\omega_i\) khác nhau. Giá trị trung bình của biến \(Y_i\), ký hiệu là \(\mu_i\) được giải thích thông qua các biến độc lập và không phụ thuộc vào \(\omega_i\), trong khi phương sai của biến \(Y_i\) phụ thuộc vào \(\omega_i\) và giá trị trung bình \(\mu_i\): \[\begin{align} \mathbb{V}(Y_i) = \cfrac{\phi}{\omega_i} \cdot V(\mu_i) \end{align}\]

Khi ước lượng mô hình tuyến tính tổng quát trên một dữ liệu cụ thể, giá trị hàm phương sai \(V\) phụ thuộc vào cách chúng ta lựa chọn hàm \(b\) và giá trị trung bình của \(Y_i\). Tham số \(\phi\) không phụ thuộc vào quan sát \(Y_i\), do đó hệ số \(w_i\) có ý nghĩa quyết định trong xác định phương sai của \(Y_i\). Chúng ta có thể lựa chọn đơn giản là cho \(w_i\) bằng 1 với mọi \(i\), nếu chúng ta tin rằng tỷ lệ \(\cfrac{V(\mu_i)}{\mathbb{V}(Y_i)}\) là hằng số. Trong một số trường hợp, \(\cfrac{V(\mu_i)}{\mathbb{V}(Y_i)}\) thay đổi theo \(i\), đó là lúc chúng ta cần lựa chọn \(w_i\) để cho kết quả tốt nhất. Chúng ta sẽ tiếp tục thảo luận về hàm phương sai của biến phụ thuộc trong phần hàm hợp lý tối đa của biến ngẫu nhiên nằm trong họ phân phối các phân phối kiểu mũ.

10.3.2 Hàm liên kết.

Hàm số liên kết \(g(\cdot)\) liên kết giá trị trung bình của biến phụ thuộc với tổ hợp tuyến tính của các biến độc lập luôn được lựa chọn trong nhóm các hàm số đơn điệu và có đạo hàm trên miền xác định của hàm số đó. Các giả thiết này đảm bảo để hàm liên kết có hàm số ngược \(g^{-1}(\cdot)\) cũng là hàm đơn điệu và có đạo hàm trên miền xác định. Một yếu tố quan trọng khác khi lựa chọn hàm liên kết đó là \(g(\cdot)\) có miền xác định trùng với miền xác định của giá trị trung bình của biến phụ thuộc và miền giá trị của \(g(\cdot)\) là tập số thực \(\mathbb{R}\).

Xin được nhắc lại rằng mối liên hệ giữa trung bình của biến phụ thuộc và tổ hợp tuyến tính của biến độc lập được mô tả thông qua hàm liên kết như sau: \[\begin{align} & g(\mu_i) = \beta_0 + \beta_1 \cdot x_{i,1} + \beta_2 \cdot x_{i,2} + \cdots + \beta_p \cdot x_{i,d} \\ & \mu_i = g^{-1}\left( beta_0 + \beta_1 \cdot x_{i,1} + \beta_2 \cdot x_{i,2} + \cdots + \beta_p \cdot x_{i,d} \right) \end{align}\]

Danh sách các hàm số thường được sử dụng làm hàm liên kết được cho trong bảng dưới đây

Tên hàm số Hàm \(g(\mu)\) Hàm \(g^{-1}(\mu)\) Miền xác định
Identily \(\mu\) \(\mu\) (-,+)
Log \(\log(\mu)\)$ \(\exp(\mu)\) (0,+)
Logit \(\log(\mu/(1-\mu)\) \(e^{\mu}/(1+e^\mu)\) (0,1)
Probit \(\Phi^{-1}(\mu)\) \(\Phi(\mu)\) (0,1)
Log-log \(\log(-\log(1-\mu))\) \(1 - \exp(-\exp(\mu))\) (0,1)
Inverse \(1/\mu\) \(1/\mu\) (-,+)/0
Inverse squared \(1/\mu^2\) \(1/\sqrt{\mu}\) (0,+)
  • Ví dụ 1: khi \(Y\) mô tả số lần khách hàng gửi yêu cầu bảo hiểm, giá trị trung bình của \(Y\) sẽ là số thực dương. Các hàm số có miền xác định là tập các số thực dương sẽ phù hợp trong trường hợp này. Có thể thấy trong bảng trên các hàm \(g(\mu) = \log(\mu)\) và hàm \(g(\mu) = 1/\mu^2\) là các hàm số có thể lựa chọn là hàm liên kết.

  • Ví dụ 2: khi \(Y\) mô tả khách hàng có gửi yêu cầu bảo hiểm hay không, hoặc mô tả sự kiện tai nạn có xảy ra hay không, giá trị trung bình của \(Y\) nằm trong khoảng \((0,1)\). Do đó các hàm \(Logit\), hàm \(Probit\), hàm \(Log-Log\), hoặc hàm \(Cauchit\) sẽ là lựa chọn phù hợp cho hàm liên kết.

Khi lựa chọn hàm liên kết bạn đọc cần cân nhắc đến khả năng giải thích của mô hình và sự khó khăn có thể gặp phải khi ước lượng của tham số trong mô hình. Ví dụ như khi biến mục tiêu chỉ nhận giá trị 0 hoặc 1, hàm logit thường xuyên được sử dụng bởi vì khả năng giải thích tốt hơn hàm \(Probit\) hay \(Log-Log\). Hoặc khi cân nhắc lựa chọn giữa hàm \(log\) và hàm \(inverse\) \(squared\), hàm \(log\) thường được ưu tiên lựa chọn. Có thể so sánh mối liên hệ giữa \(\mu_i\) với các biến độc lập như dưới đây thông qua hàm \(log\) hoặc hàm \(inverse\) \(squared\) như sau \[\begin{align} &\text{ Hàm log: } \mu_i = exp(\beta_0) \cdot exp(\beta_1 \cdot x_{i,1}) \cdots exp(\beta_p \cdot x_{i,p}) \\ &\text{ Inverse squared: } \mu_i = \cfrac{1}{\sqrt{\beta_0 + \beta_1 \cdot x_{i,1} + \cdots + \beta_p \cdot x_{i,p}}} \end{align}\] Khi sử dụng hàm \(log\), mỗi biến độc lập tác động lên giá trị trung bình của biến phụ thuộc một cách độc lập với nhau theo quy tắc nhân, trong khi đó không dễ để đánh giá tác động của một biến độc lập lên biến phụ thuộc trong công thức của hàm \(inverse\) \(squared\). Hay nói cách khác, sử dụng hàm \(log\) thường sẽ dễ dàng giải thích kết quả hơn so với khi sử dụng hàm \(inverse\) \(squared\).

Với \(Y\) là biến ngẫu nhiên nằm trong họ các phân phối kiểu mũ ta có \(\mathbb{E}(Y_i) = b^{'}(\theta_i)\). Khi chúng ta lựa chọn hàm liên kết là \(g(\cdot) = (b^{'})^{-1}(\cdot)\) thì mối liên hệ giữa tham số chính tắc \(\theta_i\) với các biến độc lập sẽ là tuyến tính. Thật vậy, \[\begin{align} & \mu_i = g^{-1}(\theta_i) \\ & g(\mu_i) = \beta_0 + \beta_1 \cdot x_{i,1} + \cdots + \beta_p \cdot x_{i,p}\\ & \rightarrow \theta_i = \beta_0 + \beta_1 \cdot x_{i,1} + \beta_2 \cdot x_{i,2} \cdots + \beta_p \cdot x_{i,p} \end{align}\]

Như vậy, với lựa chọn hàm liên kết \(g(\cdot) = (b^{'})^{-1}(\cdot)\), tham số chính tắc \(\theta_i\) của biến ngẫu nhiên phân phối mũ \(Y_i\) bằng tổ hợp tuyến tính của các biến độc lập. Trong trường hợp này, hàm liên kết \((b^{'})^{-1}(\cdot)\) còn được gọi là hàm liên kết chính tắc của biến phụ thuộc \(Y\).

  • Ví dụ 1: khi \(Y\) là biến ngẫu nhiên có phân phối chuẩn \(\mathcal{N}(\mu, \sigma)\), chúng ta có \(b(\mu) = \mu^2/2\)\(b^{'}(\mu) = \mu\) do đó hàm liên kết chính tắc của biến ngẫu nhiên phân phối chuẩn \(\mathcal{N}(\mu, \sigma)\)\(g(\mu) = \mu\).

  • Ví dụ 2: khi \(Y\) là biến ngẫu nhiên có phân phối \(gamma(\alpha,\gamma)\), chúng ta có \(b(\mu) = \log(-\mu)\), và \(b^{'}(\mu) = -1/\mu\), do đó hàm liên kết chính tắc của biến ngẫu nhiên phân phối gamma là \(g(\mu) = -1/\mu\). Trong trường hợp này hàm liên kết không có miền xác định là tập các số thực dương. Có thể thấy rằng lựa chọn hàm liên kết chính tắc không phải là một lựa chọn phù hợp.

10.4 Hàm hợp lý tối đa và ước lượng mô hình.

Hệ số tuyến tính của các biến độc lập \(\boldsymbol{\beta} = (\beta_0, \beta_1, \cdots, \beta_p)\) được ước lượng từ dữ liệu \(\textbf{y} = (y_1, y_2, \cdots y_n)\)\(\textbf{x} = (\textbf{x}_1, \textbf{x}_2, \cdots \textbf{x}_n)\) bằng phương pháp tối đa hóa hàm hợp lý, hay còn gọi là hàm likelihood. Khi biến mục tiêu \(Y_i\) có phân phối kiểu mũ, hàm likelihood được viết như sau \[\begin{align} L(\textbf{y},\boldsymbol{\beta}) = \prod\limits_{i=1}^n \ \exp\left[ \cfrac{y_i \theta - b(\theta)}{a_i(\phi)} + c_i(y_i,\phi) \right] \end{align}\] Trong hầu hết các trường hợp, chúng ta sẽ thực hiện tính toán trên hàm Log-likelihood thay vì hàm likelihood. Hàm Log-likelihood được ký hiệu \(l(\textbf{y},\beta)\) được xác định như sau

\[\begin{align} l(\textbf{y},\beta) &= \log \left( \prod\limits_{i=1}^n \ \exp\left[ \cfrac{y_i \theta_i - b(\theta_i)}{a_i(\phi)} + c_i(y_i,\phi) \right] \right) \\ &= \sum\limits_{i=1}^n \left(\cfrac{y_i \theta_i - b(\theta_i)}{a_i(\phi)} + c_i(y_i,\phi)\right) \end{align}\] Véc-tơ hệ số \(\boldsymbol{\beta}\) được ước lượng sao cho giá trị của hàm Log-likelihood đạt giá trị lớn nhất. Như vậy \(\boldsymbol{\beta}\) là nghiệm của hệ phương trình \[\begin{align} \cfrac{\partial l(\textbf{y},\boldsymbol{\beta})}{\partial \beta_j} & = 0 \ \ \forall j = 1,2, \cdots, p \end{align}\] Lưu ý rằng \(b^{'}(\theta_i) = \mu_i\) nên trong các thành phần của hàm mật độ xác suất của biến \(Y_i\) chỉ có hàm số \(b(\cdot)\) và tham số \(\theta_i\) phụ thuộc vào hệ số \(\boldsymbol{\beta}\). Đạo hàm của hàm Log-likelihood theo \(\beta\) có thể viết được như sau \[\begin{align} \cfrac{\partial l(\textbf{y},\beta)}{\partial \beta_j} & = \sum\limits_{i=1}^n \cfrac{1}{a_i(\phi)} \left(y_i \cfrac{\partial \theta_i}{\partial \beta_j} - \cfrac{\partial b(\theta_i)}{\partial \beta_j} \right) \\ & = \sum\limits_{i=1}^n \cfrac{y_i - b^{'}(\theta_i)}{a_i(\phi)} \cfrac{\partial \theta_i}{\partial \beta_j} \end{align}\]

Lưu ý rằng \(\mu_i = b^{'}(\theta_i)\), đo đó \[\begin{align} \theta_i = (b^{'})^{-1}(\mu_i) = (b^{'})^{-1}\left(g^{-1}(\beta_0 + \beta_1 \cdot x_{i,1} + \cdots \beta_p \cdot x_{i,p})\right) \end{align}\] đồng thời, đạo hàm của hàm ngược được của các hàm \(b^{'}(\cdot)\)\(g(\cdot)\) được xác định như sau \[\begin{align} & \left((b^{'})^{-1}\right)^{'}(\mu_i) = \cfrac{1}{(b^{''}\left((b^{'})^{-1}(\mu_i)\right)} = \cfrac{1}{b^{''}(\theta_i)} \\ & (g^{-1}(\psi_i))^{'} = \cfrac{1}{(g^{'}(g^{-1}(\psi_i))} = \cfrac{1}{g^{'}(\mu_i)} \end{align}\] với \(\psi_i = \beta_0 + \beta_1 \cdot x_{i,1} + \cdots \beta_p \cdot x_{i,p}\).

Như vậy, đạo hàm của \(\theta_i\) theo \(\beta_j\) được tính như sau \[\begin{align} \cfrac{\partial \theta_i}{\partial \beta_j} = \cfrac{\partial (b^{'})^{-1}(g^{-1}(\psi_i(\beta_j)))}{\partial \beta_j} = \cfrac{x_{i,j}}{b^{''}(\theta_i) \cdot g^{'}(\mu_i)} \end{align}\]

Ta có đạo hàm của hàm Log-likelihood theo các tham số \(\beta_j\) \[\begin{align} \cfrac{\partial l(\textbf{y},\beta)}{\partial \beta_j} = \sum\limits_{i=1}^n \cfrac{(y_i - \mu_i) \cdot x_{i,j}}{a_i(\phi) \cdot b^{''}(\theta_i) \cdot g^{'}(\mu_i)} \end{align}\]

Với lựa chọn \(a_i(\phi) = \cfrac{\phi}{\omega_i}\), do phương sai của biến phụ thuộc là \(a_i(\phi) \cdot b^{''}(\theta_i)\) nên ta có thể viết hàm Log-likelihood theo các tham số \(\beta_j\) theo giá trị trung bình và phương sai của biến phụ thuộc \[\begin{align} \cfrac{\partial l(\textbf{y},\boldsymbol{\beta})}{\partial \beta_j} &= \sum\limits_{i=1}^n \cfrac{(y_i - \mu_i) \cdot x_{i,j}}{\mathbb{V}(y_i) \cdot g^{'}(\mu_i)} \\ & = \sum\limits_{i=1}^n w_i \cfrac{(y_i - \mu_i) \cdot x_{i,j}}{\phi V(\mu_i) \cdot g^{'}(\mu_i)} \\ & =\cfrac{1}{\phi} \sum\limits_{i=1}^n w_i \cfrac{x_{i,j}}{V(\mu_i) \cdot g^{'}(\mu_i)} (y_i - \mu_i) \end{align}\] Có thể thầy rằng:

  • Thứ nhất, \(\beta_j\) là nghiệm của phương trình \(\cfrac{\partial l(\textbf{y},\beta)}{\partial \beta_j} = 0\) sẽ không phụ thuộc vào giá trị của tham số phân tán \(\phi\).

  • Thứ hai, giá trị của \(\mu_i\) phụ thuộc vào giá trị của các biến độc lập \(\textbf{x}_i\) và phụ thuộc vào các hệ số \(\boldsymbol{\beta}\), do đó giá trị \(\cfrac{x_{i,j}}{V(\mu_i) \cdot g^{'}(\mu_i)}\) không phụ thuộc hoàn toàn vào cách chúng ta xây dựng mô hình.

  • Thứ ba, giá trị \(w_i\) không phụ thuộc vào dữ liệu do đó có thể được sử dụng linh hoạt để ước lượng mô hình có kết quả tốt nhất.

Giải hệ phương trình với ẩn là véc-tơ tham số \(\boldsymbol{\beta}\) như trên thường phải sử dụng các phương pháp giải số. Phương pháp thường được sử dụng là thuật toán Newton Raphson.

Hàm glm() sử dụng xuyên suốt trong chương sách cho phép bạn đọc ước lượng mô hình tuyến tính tổng quát cho đa số các phân phối thường gặp của \(Y_i\). Tham số \(weight\) trong hàm glm() là véc-tơ tham số \(w_i\) như chúng ta đã trình bày ở trên. Lựa chọn giá trị cho \(w_i\) hoàn toàn do cách tiếp cận của người xây dựng mô hình.

  • Ví dụ 1: khi lựa chọn \(Y_i\) có phân phối Poisson với tham số \(\lambda_i = exp(\theta_i)\) và hàm liên kết là hàm \(g(\cdot) = log(\cdot)\). Chúng ta có \(a(\phi) = 1\) do đó \(w_i = 1 \forall i\). \[\begin{align} & g(\mu_i) = log(\mu_i) \rightarrow g^{'}(\mu_i) = 1/\mu_i \\ & b(\theta) = exp(\theta) \rightarrow b^{'}(\theta) = b^{''}(\theta) = exp(\theta) \rightarrow (b^{'})^{-1}(\theta) = \log(\theta) \rightarrow V(\theta) = b^{''}(b^{'})^{-1})(\theta) = \theta \end{align}\] Đạo hàm của hàm Log-likelihood theo \(\beta_j\) trở thành \[\begin{align} \sum\limits_{i=1}^n w_i \cdot \cfrac{x_{i,j}}{\phi V(\mu_i) \cdot g^{'}(\mu_i)} (y_i - \mu_i) &= \sum\limits_{i=1}^n x_{i,j} (y_i - \mu_i) \\ &= \sum\limits_{i=1}^n x_{i,j} \left[y_i - \exp(\beta_0 + \beta_1 \cdot x_{i,1} + \cdots + \beta_p \cdot x_{i,p})\right] \end{align}\] hay nói một cách khác, véc-tơ tham số \(\beta\) để tối đa hóa giá trị của hàm Log-likelihood là nghiệm của hệ phương trình \[\begin{align} \sum\limits_{i=1}^n x_{i,j} \left[y_i - \exp \left(\beta_0 + \beta_1 \cdot x_{i,1} + \cdots + \beta_p \cdot x_{i,p} \right) \right] = 0 \ \forall j \end{align}\]

  • Ví dụ 2: khi \(Y_i\) có phân phối Gamma với tham số \(\alpha\), \(\gamma_i\) chúng ta có \(b(\theta_i) = log(-\theta_i)\) với \(\theta_i = - \cfrac{\gamma_i}{\alpha}\) đồng thời \(a(\phi) = \phi\) do đó \(w_i = 1\) \(\forall i\). Giả sử hàm liên kết được lựa chọn là hàm \(log\): \(g(\cdot) = log(\cdot)\). \[\begin{align} & g(\mu_i) = log(\mu_i) \rightarrow g^{'}(\mu_i) = 1/\mu_i \\ & b(\theta_i) = log(-\theta_i) \rightarrow b^{'}(\theta_i) = -1/\theta_i \rightarrow b^{''}(\theta) = 1/\theta^2 \rightarrow (b^{'})^{-1}(\theta) = -1/\theta \rightarrow V(\mu) = b^{''}(b^{'})^{-1})(\mu) = \mu^2 \end{align}\] Đạo hàm của hàm Log-likelihood theo \(\beta_j\) trở thành \[\begin{align} \sum\limits_{i=1}^n w_i \cdot \cfrac{x_{i,j}}{\phi V(\mu_i) \cdot g^{'}(\mu_i)} (y_i - \mu_i) &= \sum\limits_{i=1}^n \cfrac{x_{i,j}}{\mu_i} (y_i - \mu_i) \end{align}\] véc-tơ tham số \(\boldsymbol{\beta}\) để tối đa hóa giá trị của hàm Log-likelihood là nghiệm của hệ phương trình \[\begin{align} \sum\limits_{i=1}^n x_{i,j} \left\{y_i \cdot \exp\left[-(\beta_0 + \beta_1 \cdot x_{i,1} + \cdots + \beta_p \cdot x_{i,p}) \right] - 1 \right\} = 0 \ \forall j \end{align}\]

  • Ví dụ 3: khi \(Y_i\) là số tiền bồi thường trung bình cho 1 tai nạn trong vòng 1 năm của một khách hàng, biết rằng khách hàng được bồi thường \(n_i\) lần trong năm và số tiền bồi thường của một vụ tai nạn là biến ngẫu nhiên \(Y^*_i\) phân phối Gamma với tham số \(\alpha\), \(\beta_i\). Do \(Y_i\) là giá trị trung bình của \(n_i\) biến phân phối Gamma độc lập với cùng tham số \(\alpha\), \(\beta_i\) nên \(Y_i\) sẽ có phân phối Gamma với tham số \((n_i \alpha)\)\((n_i \beta_i)\). Khi viết \(Y_i\) dưới dạng phân phối kiểu mũ, chúng ta có \(b(\theta_i) = log(-\theta_i)\) với \(\theta_i = - \cfrac{\beta_i}{\alpha}\)\(a(\phi) = \phi/n_i\). Véc-tơ tham số \(\boldsymbol{\beta}\) để tối đa hóa giá trị của hàm Log-likelihood là nghiệm của hệ phương trình \[\begin{align} \sum\limits_{i=1}^n n_i \cdot \ x_{i,j} \cdot \left[y_i \cdot \exp\left(-(\beta_0 + \beta_1 \cdot x_{i,1} + \cdots + \beta_p \cdot x_{i,p}) \right) - 1 \right] = 0 \ \forall j \end{align}\]

10.5 So sánh và lựa chọn mô hình tuyến tính tổng quát.

Nắm được các nguyên tắc chung bạn đọc có thể tự xây dựng nhiều mô hình tuyến tính tổng quát khác nhau cho một dữ liệu cụ thể. Thách thức đặt ra là một mô hình liệu có thực sự có tốt hơn các mô hình khác, hay trong số các mô hình bạn lựa chọn mô hình nào là phù hợp nhất? Phần này của chương sẽ thảo luận về vấn đề so sánh mô hình và lựa chọn mô hình. Các chỉ tiêu thống kê được sủ dụng để so sánh mô hình sẽ xoay quanh giá trị của hàm hợp lý tối đa, bao gồm có thước đo deviance, chỉ tiêu AIC, AICC, hay BIC.

10.5.1 Thước đo deviance

Thước đo được gọi là deviance thường được sử dụng để đánh giá và so sánh các mô hình tuyến tính tổng quát có cùng phân phối của biến độc lập. Chỉ tiêu này được tính toán dựa trên giá trị hàm Log-likelihood. Khi \(Y\) là biến ngẫu nhiên trong nhóm các phân phối mũ, hàm log-likelihood được viết như sau \[\begin{align} l(\textbf{y},\theta) & = \sum\limits_{i=1}^n \left(\cfrac{y_i \theta_i - b(\theta_i)}{a_i(\phi)} + c_i(y_i,\phi)\right) \end{align}\] với \(\boldsymbol{\theta}\) là véc-tơ các tham số chính tắc, \(\boldsymbol{\theta} = (\theta_1, \theta_2, \cdots, \theta_n)\). Sau khi phân phối của \(Y\) và hàm liên kết được lựa chọn, chúng ta ước lượng được véc-tơ tham số \(\boldsymbol{\beta}\) là hệ số của các biến độc lập bằng cách tối đa hóa hàm log-likelihood. Sau khi đã xác định được véc-tơ tham số \(\boldsymbol{\beta}\), với mỗi lựa chọn cho phân phối của \(Y\) và hàm liên kết, chúng ta sẽ tính toán được các tham số chính tắc của mô hình tuyến tính tổng quát. Nếu hàm liên kết được lựa chọn là hàm liên kết chính tắc, \(g(\cdot) = (b^{'})^{-1}(\cdot)\), chúng ta có các tham số chính tắc chính là tổ hợp tuyến tính của các biến độc lập \(\boldsymbol{\theta}^M = (\theta^M_1, \theta^M_2, \cdots, \theta^M_n)\) với \[\begin{align} \theta^M_i = \beta_0 + \beta_1 \cdot x_{i,1} + \cdots + \beta_p \cdot x_{i,p} (\#eq:compa1) \end{align}\]

Với mỗi lựa chọn cho mô hình tuyến tính tổng quát, bao gồm lựa chọn cho phân phối của \(Y\) và hàm liên kết \(g\), tạm gọi là mô hình \(M\), chúng ta gọi \(l(\textbf{y},\boldsymbol{\theta}^M)\) là giá trị của hàm Log-likelihood tại tham số \(\boldsymbol{\theta}^M\) được xác định qua phương trình @ref(eq:compa1). Lưu ý rằng véc-tơ \(\boldsymbol{\theta}^M\) có độ dài là \(n\), bằng với kích thước của dữ liệu, tuy nhiên các tham số này được tính toán từ \((p+1)\) giá trị \(\beta\) ước lượng được và giá trị của biến độc lập.

Điều gì xảy ra nếu tham số chính tắc \(\boldsymbol{\theta}\) hoàn toàn tự do và không phụ thuộc vào biến độc lập? Hàm Log-likelihood sẽ đạt giá trị cực đại tại \(\theta^S = (\theta^S_1, \theta^S_2, \cdots, \theta^S_n)\) với \(\theta^S_i\) là giá trị sao cho đạo hàm của hàm Log-likelihood tại \(\theta^S_i\) bằng 0. Do \(\theta\) hoàn toàn tự do nên chỉ có thành phần thứ \(i\) của hàm Log-likelihood phụ thuộc vào \(\theta^S_i\) \[\begin{align} \cfrac{\partial l(\textbf{y},\theta)}{\partial \theta_i} & = \cfrac{y_i - b^{'}(\theta_i)}{a_i(\phi)} \end{align}\] Cho đạo hàm của \(l(\textbf{y},\theta)\) theo \(\theta_i\) bằng 0 chúng ta có \(y_i - b^{'}(\theta^S_i) = 0\) hay \(\theta^S_i = (b^{'})^{-1}(y_i)\).

Bạn đọc lưu ý rằng giá trị hàm Log-likelihood đạt cực đại tại \(\boldsymbol{\theta}^S\) không có nghĩa là tham số \(\boldsymbol{\theta}^S\) là lựa chọn tốt nhất cho mô hình tuyến tính tổng quát bởi tham số có đến \(n\) bậc tự do. \(\boldsymbol{\theta}^S\) cho chúng ta thông tin về giá trị cận trên của \(l(\textbf{y},\theta)\) khi \(\theta\) thay đổi. Thước đo deviance của mô hình \(M\), ký hiệu \(D*(y,\theta^M)\) được định nghĩa là hai lần khoảng cách từ \(l(\textbf{y},\boldsymbol{\theta}^M)\) đến giá trị tối đa \(l(\textbf{y},\boldsymbol{\theta}^S)\). Deviance của một mô hình cho biết mô hình được lựa chọn gần với phân phối quan sát được như thế nào, và mô hình có deviance càng nhỏ thì càng giải thích tốt hơn biến phụ thuộc \[\begin{align} D^{*}(y, \theta^M) &= 2 \left( l(\textbf{y},\theta^S) - l(\textbf{y},\theta^M) \right) \\ & = 2 \sum\limits_{i=1}^n \cfrac{y_i (\theta^S_i - \theta^M_i) - (b(\theta^S_i) - b(\theta^M_i))}{a_i(\phi)} \end{align}\]

Trong trường hợp hàm \(a_i(\phi)\) là tuyến tính theo \(\phi\); \(a_i(\phi) = \cfrac{\phi}{w_i}\), ta có \[\begin{align} D^{*}(y, \theta^M) & = \cfrac{1}{\phi} \sum\limits_{i=1}^n 2w_i \cdot \left[y_i (\theta^S_i - \theta^M_i) - (b(\theta^S_i) - b(\theta^M_i)) \right] = \cfrac{D(y, \theta^M)}{\phi} \end{align}\] với \[\begin{align} D(y, \theta^M) & = \sum\limits_{i=1}^n 2w_i \cdot \left[y_i (\theta^S_i - \theta^M_i) - (b(\theta^S_i) - b(\theta^M_i)) \right] \end{align}\] trong đó \(D(y, \theta^M)\) là thước đo deviance bỏ qua ảnh hưởng của tham số dispersion \(\phi\).

  • Ví dụ 1: biến mục tiêu \(Y_i\) có phân phối chuẩn: \(Y_i \sim \mathcal{N}(\mu_i, \sigma)\) và hàm liên kết \(g(x) = x\). Có thể viết hàm mật độ của \(Y_i\) dưới dạng phân phối mũ như sau \[\begin{align} f(y, \theta_i, \phi) & = \exp\left[ \cfrac{\theta_i y - \theta_i^2/2} {\phi} - \cfrac{y^2}{2\phi} - \cfrac{1}{2} log(2 \pi \phi) \right] \end{align}\] với \(\theta_i = \mu_i\)\(\phi = \sigma^2\). Ta có \(\theta^S_i = (b^{'})^{-1}(y_i) = y_i\), do đó deviance tính trên quan sát \(\textbf{y}\) được xác định như sau \[\begin{align} D(y, \theta^M) & = \sum\limits_{i=1}^n 2 \cdot \left[y_i (\theta^S_i - \theta^M_i) - (b(\theta^S_i) - b(\theta^M_i)) \right] \\ & = \sum\limits_{i=1}^n (y_i - \theta^M_i)^2 \\ & = \sum\limits_{i=1}^n \left[y_i - (\beta_0 + \beta_1 \cdot x_{i,1} + \cdots + \beta_p \cdot x_{p,1}) \right]^2 \end{align}\] Trong trường hợp hồi quy tuyến tính thông thường, deviance chính là tổng bình phương sai số.

  • Ví dụ 2: biến mục tiêu \(Y_i\) có phân phối Poisson \(Y_i \sim \mathcal{P}(\lambda_i)\) và hàm liên kết \(g(\cdot) = log(\cdot)\). \[\begin{align} f(y; \theta_i) = exp\left[ \cfrac{\theta_i y - exp(\theta_i)}{1} - log(\Gamma(y+1)) \right] \text{ với } \lambda_i = exp(\theta_i) \end{align}\] Ta có \(\theta^S_i = (b^{'})^{-1}(y_i) = log(y_i)\), do đó deviance tính trên quan sát \(\textbf{y}\) được xác định như sau \[\begin{align} D(y, \theta^M) & = \sum\limits_{i=1}^n 2 \cdot \left[y_i (\theta^S_i - \theta^M_i) - (b(\theta^S_i) - b(\theta^M_i)) \right] \\ & = \sum\limits_{i=1}^n 2 \cdot \left[y_i (log(y_i) - \theta^M_i) - (y_i - exp(\theta^M_i)) \right] \\ & = \sum\limits_{i=1}^n 2 \cdot \left[y_i \left(log(y_i) - log(\mu^M_i)\right) - (y_i - \mu^M_i) \right] \end{align}\] với \(\mu^M_i = \exp(\beta_0 + \beta_1 \cdot x_{i,1} + \cdots + \beta_p \cdot x_{p,1})\).

Kết quả ước lượng khi sử dụng hàm glm() trong R hiển thị hai giá trị là Null deviance và Residual deviance. Null deviance được tính toán với giả thiết là chỉ có hệ số chặn \(\beta_0\), còn Residual deviance được tính toán với véc-tơ \(\boldsymbol{\beta}\) đầy đủ.

Giá trị Residual deviance ngoài sử dụng để so sánh hai mô hình có cùng phân phối của biến phụ thuộc còn được sử dụng để lựa chọn biến trong mô hình. Giả sử hai mô hình \(M1\)\(M2\) có cùng phân phối cho biến phụ thuộc và có cùng hàm liên kết \(g(.)\), mô hình \(M1\)\(m_1\) biến độc lập trong khi mô hình \(M_2\)\(m_2\) biến độc lập, bao gồm tất cả các biến độc lập của mô hình \(M1\). Nói một cách khác, \(M_2\) có nhiều hơn \(M_1\)\((m_2 - m_1)\) biến độc lập. Mô hình \(M_2\) sẽ có deviance lớn hơn mô hình \(M_1\) do có nhiều biến độc lập hơn, tuy nhiên việc thêm \((m_2 - m_1)\) biến độc lập vào mô hình có ý nghĩa thống kê nếu hiệu số giữa \(D^*(y, \theta^{M_2})\)\(D^*(y, \theta^{M_1})\) là đủ lớn.

Có thể chứng minh được rằng khi kích thước dữ liệu đủ lớn, hiệu số giữa \(D^*(y, \theta^{M_2})\)\(D^*(y, \theta^{M_1})\) là một biến ngẫu nhiên phân phối xấp xỉ phân phối \(\chi^2\) với bậc tự do \((m_2 - m_1)\) \[\begin{align} D^{*}(y, \theta^{M_1}) - D^{*}(y, \theta^{M_2}) = 2 \cdot \log\left( \cfrac{L(y,\theta^{M_2})}{L(y,\theta^{M_1})} \right) \sim \chi^2(m_2 - m_1) \end{align}\]

  • Ví dụ 3: trong dữ liệu \(exposure.csv\), khi biến \(Claim\_Count\) được giả thiết có phân phối Poisson và giá trị trung bình được giải thích bằng độ tuổi, hoặc giới tính của người được bảo hiểm, hoặc cả hai biến. Chúng ta gọi mô hình \(M_1\) là mô hình mà giá trị trung bình của biến phụ thuộc được giải thích bằng biến độ tuổi, mô hình \(M_2\) là mô hình mà giá trị trung bình của biến phụ thuộc được giải thích bằng cả biến giới tính, và mô hình \(M_3\) là mô hình mà biến phụ thuộc phụ thuộc vào cả độ tuổi và giới tính của khách hàng.
dat<-read.csv("../KHDL_KTKD/Dataset/exposure.csv")

glm_M1<-glm(Claim_Count~Age, family = poisson(link="log"), data = dat)
glm_M2<-glm(Claim_Count~Gender, family = poisson(link="log"), data = dat)
glm_M3<-glm(Claim_Count~Age+Gender, family = poisson(link="log"), data = dat)

summary(glm_M1)
summary(glm_M2)
summary(glm_M3)
Giá trị residual deviance của ba mô hình được tổng hợp ở bảng dưới đây
Mô hình Biến phụ thuộc Deviance
\(M_1\) Age 8324.2
\(M_2\) Gender 8540.5
\(M_3\) Age, Gender 8316.8

Mô hình \(M_1\)\(M_2\) đều chỉ có một biến độc lập và \(M_1\) có deviance nhỏ hơn \(M_2\) do đó mô hình \(M_1\) tốt hơn \(M_2\). So sánh mô hình \(M_1\) với 1 biến độc lập là \(Age\) với mô hình \(M_3\) có 2 biến độc lập là \(Age\)\(Gender\) cần dựa trên kiểm định \(\chi^2\). Ta có \(D^{*}(y, \theta^{M_1}) - D^{*}(y, \theta^{M_2}) = 8324.2 - 8316.8 = 7.4\). Giá trị 7.4 tương ứng với mức xác suất \(0.994\) của phân phối \(\chi^2(1)\). Điều này có ý nghĩa là ở mức độ tin cậy 99%, có thể kết luận rằng thêm biến \(Gender\) vào mô hình \(M_1\) là có ý nghĩa thống kê, tuy nhiên việc thêm biến \(Gender\) lại không có ý nghĩa thống kê ở mức độ tin cậy \(99.5\%\).

10.5.2 Giá trị hàm log-likelihood, AIC và BIC

Deviance là một thước đo hữu ích trong so sánh các mô hình có cùng phân phối của biến phụ thuộc và hàm liên kết. Khi các mô hình cần so sánh không có chung phân phối của biến phụ thuộc thì hiệu giữa hai hàm log-likelihood sẽ không có phân phối \(\chi^2\) và khi đó việc so sánh các mô hình là không thế thực hiện được.

Trong phần trước, chúng ta đã định nghĩa \(l(y,\theta^M)\) là giá trị tối đa của hàm log-likelihood cho mô hình M với tập hợp các biến độc lập đã lựa chọn. Nhìn chung, giá trị của \(l(y,\theta^M)\) không cho biết nhiều thông tin về độ phù hợp của mô hình, nhưng người xây dựng mô hình thường mong muốn giá trị này càng lớn thì càng tốt. Tuy nhiên, có một vấn đề khi so sánh trực tiếp giá trị \(l(y,\theta^M)\) giữa các mô hình là: một mô hình bao gồm nhiều biến độc lập hơn thì rất có thể có giá trị \(l(y,\theta^M)\) lớn hơn. Để giải quyết vấn đề này, có ba thước đo được điều chỉnh từ \(l(y,\theta^M)\) thường được sử dụng cho mô hình tuyến tính tổng quát với mục đích đánh giá và so sánh giữa các mô hình là AIC, AICC và BIC.

Chỉ tiêu AIC, viết tắt của Akaike information criterion, là chỉ tiêu đơn giản nhất được tính toán từ công thức như sau \[\begin{align} AIC = 2 \left(-l(y,\theta^M)+r\right) \end{align}\] trong đó \(r\) là số lượng tham số cần ước lượng trong mô hình. Giá trị \(AIC\) càng nhỏ thì mô hình càng khớp hơn với dữ liệu.

Chỉ tiêu AICC (hoặc AICs) được điều chỉnh từ AIC, được sử dụng khi kích thước dữ liệu không quá lớn để thay thế cho AIC, và được tính bởi công thức như sau \[\begin{align} AICC = AIC + \cfrac{2r(r+1)}{n-r-1} \end{align}\] Khi kích thước dữ liệu \(n\) lớn, AIC và AICC sẽ tương đương nhau do \(\cfrac{2r(r+1)}{n-r-1}\) nhỏ.

Chỉ tiêu thứ ba là BIC, là viết tắt của Bayesian information criterion. Trong một vài tài liệu BIC còn được gọi là SBC. Cũng giống như AIC và AICC, BIC là chỉ tiêu được tính toán từ giá trị cực đại của hàm log-likelihood điểu chỉnh để phản ánh số lượng tham số sử dụng trong mô hình là nhiều hay ít \[\begin{align} BIC = 2 \left(-l(y,\theta^M)+r log(n)\right) \end{align}\]

Cơ sở lý thuyết cho các chỉ tiêu AIC, AICC, và BIC bạn đọc có thể tìm thấy trong bất kỳ tài liệu thống kê toán nào, do đó chúng tôi sẽ không trình bày trong cuốn sách này. Chúng tôi muốn nhấn mạnh vào góc độ ứng dụng của các chỉ tiêu này khi sử dụng để so sánh các mô hình.

  • Ví dụ 1: chúng ta quay trở lại dữ liệu “MotoInsurance.csv” khi biến mục tiêu \(Y\) chỉ nhận hai giá trị là “No” hoặc “Yes”. Biến \(Y\) có thể là biến dạng nhị phân hoặc vừa có thể là biến dạng đếm nếu chúng ta cho tương đương giá trị “No” tương đương với 0 và giá trị “Yes” tương ứng với 1. Khi xây dựng mô hình tuyến tính tổng quát, chúng ta có thể sử dụng phân phối Poisson cho biến mục tiêu với hàm liên kết \(g(\cdot) = log(\cdot)\).
dat<-read.csv("../KHDL_KTKD/Dataset/MotoInsurance.csv")
dat$Y<-ifelse(dat$Y=="Yes",1,0)
dat$sex<-as.factor(dat$sex)
dat$urban<-as.factor(dat$urban)
glm.pois<-glm(Y~age+sex+urban+seniority,data=dat, family = poisson(link = "log"))
summary(glm.pois)
## 
## Call:
## glm(formula = Y ~ age + sex + urban + seniority, family = poisson(link = "log"), 
##     data = dat)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -2.0824  -0.7362  -0.5465   0.5711   1.9556  
## 
## Coefficients:
##              Estimate Std. Error z value Pr(>|z|)    
## (Intercept) -0.286511   0.105615  -2.713  0.00667 ** 
## age         -0.034131   0.002546 -13.406  < 2e-16 ***
## sexM        -0.491418   0.057331  -8.572  < 2e-16 ***
## urban1       0.625032   0.053999  11.575  < 2e-16 ***
## seniority    0.070888   0.004257  16.652  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for poisson family taken to be 1)
## 
##     Null deviance: 2938.1  on 3999  degrees of freedom
## Residual deviance: 2418.7  on 3995  degrees of freedom
## AIC: 5202.7
## 
## Number of Fisher Scoring iterations: 5

Do \(Y\) là biến nhị phân nên một cách tự nhiên, bạn đọc sẽ cân nhắc sử dụng phân phối nhị phân cho \(Y\). Chúng ta xây dựng mô hình tuyến tính tổng quát với phân phối nhị thức cho \(Y\) và hàm liên kết là hàm \(logit\). Thay vì sử dụng cả 4 biến độc lập, chúng ta chỉ sử dụng hai biến độc lập là \(age\)\(sex\),

dat$Y<-as.factor(dat$Y) # đổi biến Y thành factor
glm.binom.logit<-glm(Y~age+sex,data=dat, family = binomial(link = "logit"))
summary(glm.binom.logit)
## 
## Call:
## glm(formula = Y ~ age + sex, family = binomial(link = "logit"), 
##     data = dat)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -1.4544  -0.9261  -0.7396   1.2607   2.0088  
## 
## Coefficients:
##              Estimate Std. Error z value Pr(>|z|)    
## (Intercept)  1.109828   0.130876   8.480   <2e-16 ***
## age         -0.026603   0.002709  -9.822   <2e-16 ***
## sexM        -0.723479   0.076585  -9.447   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 5163.3  on 3999  degrees of freedom
## Residual deviance: 4935.3  on 3997  degrees of freedom
## AIC: 4941.3
## 
## Number of Fisher Scoring iterations: 4

Tương tự như mô hình \(glm.pois\), mô hình \(glm.binom.logit\) cũng có tất cả các hệ số của biến độc lập khác 0. Làm thế nào để biết rằng mô hình \(glm.pois\) sử dụng phân phối Poisson với 4 biến độc lập hay mô hình \(glm.binom.logit\) với phân phối nhị thức cho \(Y\) và 2 biến độc lập là tốt hơn? Nếu chúng ta sử dụng thước đo deviance thì kết quả sẽ không chính xác vì các mô hình có phân phối của biến phụ thuộc khác nhau. Chỉ tiêu \(AIC\) được tính toán sẵn từ hàm \(glm\) có thể được sử dụng để so sánh hai mô hình trong trường hợp này. Chỉ tiêu AIC của \(glm.pois\) là 5202.7 trong khi chỉ tiêu AIC của \(glm.binom.logit\) là 4941.3. Mô hình \(glm.binom.logit\) có AIC nhỏ hơn nên sẽ tốt hơn để mô hình hóa dữ liệu trong trường hợp này.

  • Ví dụ 2: chúng ta sẽ tiếp tục với dữ liệu “MotoInsurance.csv”. Một cách tự nhiên, khi sử dụng phân phối nhị thức cho biến phụ thuộc, bạn đọc sử dụng hàm liên kết là hàm \(logit\). Liệu lựa chọn này có là tốt nhất để mô hình hóa dữ liệu mà chúng ta đang nghiên cứu? Bạn đọc có thể sử dụng chỉ tiêu AIC để lựa chọn hàm liên kết phù hợp. Chúng ta sẽ sử dụng 4 biến độc lập là \(age\), \(sex\), \(urban\), và \(seniority\) để giải thích giá trị trung bình của biến phụ thuộc khi xây dựng mô hình
glm.binom.logit<-glm(Y~age+sex+seniority+urban,data=dat, family = binomial(link = "logit"))
glm.binom.probit<-glm(Y~age+sex+seniority+urban,data=dat, family = binomial(link = "probit"))
summary(glm.binom.logit)
## 
## Call:
## glm(formula = Y ~ age + sex + seniority + urban, family = binomial(link = "logit"), 
##     data = dat)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -2.4424  -0.8115  -0.5046   1.0440   2.7439  
## 
## Coefficients:
##              Estimate Std. Error z value Pr(>|z|)    
## (Intercept)  0.793757   0.144541   5.492 3.98e-08 ***
## age         -0.058754   0.003525 -16.668  < 2e-16 ***
## sexM        -0.983040   0.085377 -11.514  < 2e-16 ***
## seniority    0.133569   0.006808  19.618  < 2e-16 ***
## urban1       1.174515   0.078060  15.046  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 5163.3  on 3999  degrees of freedom
## Residual deviance: 4295.8  on 3995  degrees of freedom
## AIC: 4305.8
## 
## Number of Fisher Scoring iterations: 4
summary(glm.binom.probit)
## 
## Call:
## glm(formula = Y ~ age + sex + seniority + urban, family = binomial(link = "probit"), 
##     data = dat)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -2.4931  -0.8168  -0.4987   1.0639   2.9359  
## 
## Coefficients:
##              Estimate Std. Error z value Pr(>|z|)    
## (Intercept)  0.449579   0.085724   5.244 1.57e-07 ***
## age         -0.034482   0.002009 -17.163  < 2e-16 ***
## sexM        -0.577303   0.050821 -11.360  < 2e-16 ***
## seniority    0.077888   0.003890  20.021  < 2e-16 ***
## urban1       0.697759   0.046129  15.126  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 5163.3  on 3999  degrees of freedom
## Residual deviance: 4292.4  on 3995  degrees of freedom
## AIC: 4302.4
## 
## Number of Fisher Scoring iterations: 5

Mô hình \(glm.binom.probit\) có deviance và AIC nhỏ hơn \(glm.binom.logit\), điều này cho biết sử dụng hàm liên kết \(probit\) sẽ cho kết quả tốt hơn so với hàm liên kết \(logit\).

11 REFERENCE

1. Annette J. Dobson and Adrian G. Barnett (2018). An Introduction to Generalized Linear Models.
2. Alan Agresti. (2015). Foundations of Linear and Generalized Linear Models.

## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
## 
## Attaching package: 'kableExtra'
## The following object is masked from 'package:dplyr':
## 
##     group_rows
## 
## Attaching package: 'gridExtra'
## The following object is masked from 'package:dplyr':
## 
##     combine

12 Mô hình dạng cây

12.1 Cây quyết định và bài toán hồi quy

12.2 Cây quyết định trong bài toán phân loại

12.3 Các kỹ thuật kết hợp nhiều cây quyết định

12.4 Thực hành:

12.4.1 So sánh random forest, boosting, và mô hình tuyến tính trên dữ liệu Sales.

12.4.2 So sánh random forest, boosting, và logistic regression trên dữ liệu tín dụng khách hàng.

12.4.3 So sánh random forest, boosting, và Poisson regression trên dữ liệu bồi thường bảo hiểm.

## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
## 
## Attaching package: 'kableExtra'
## The following object is masked from 'package:dplyr':
## 
##     group_rows
## 
## Attaching package: 'gridExtra'
## The following object is masked from 'package:dplyr':
## 
##     combine

13 Mô hình mạng neural network

Chương sách này thảo luận một chủ đề quan trọng có ứng dụng rộng rãi nhất trong lĩnh vực trí tuệ nhân tạo, đó là mô hình mạng học sâu. Tại thời điểm nhóm tác giả viết cuốn sách (2023), mạng học sâu là một lĩnh vực nghiên cứu tích cực nhất không chỉ trong khoa học máy tính, công nghệ thông tin mà còn cả trong các lĩnh vực khác như kinh tế, tài chính, y tế, xây dựng,… Nền tảng của mô hình mạng học sâu là mô hình mạng thần kinh nhân tạo (Neural Network). Mô hình neural network đã được biết đến đến rộng rãi vào cuối những năm 1980 bởi cách vận hành của mô hình mô tả lại cách thức mà hệ thống thần kinh của con người xử lý thông tin. Mặc dù các đặc tính của mô hình Neural Network được phân tích bởi những nhà toán học và nhà thống kê và các thuật toán liên quan đến mô hình này đã được cải thiện nhưng sau đó, do với sự ra đời của các phương pháp học máy khác như SVM, random forest, boosting,… các mô hình mạng nơ-ron phần nào không được ưa chuộng.

Từ những năm 2010, với nhu cầu xử lý các dữ liệu ngày càng phức tạp và sự ra đời của các kiến trúc máy tính lớn, mô hình mạng nơ-ron đã quay trở lại với tên mới là mạng học sâu (deep learning). Mạng học sâu vượt trội hoàn toàn các mô hình học máy thông thường trong phân loại hình ảnh/video và mô hình hóa ngôn ngữ tự nhiên bao gồm dữ liệu kiểu văn bản và lời nói (Natural Langugue Processing). Các nhà khoa học trong lĩnh vực này tin rằng lý do chính cho những thành công của mô hình mạng nơ-ron là sẵn có của các bộ dữ liệu để huấn luyện môn hình và cấu trúc của mô hình mạng nơ-ron có thể đáp ứng được với bất kỳ tập dữ liệu khổng lồ nào.

13.1 Mạng neural với một layer duy nhất

13.1.1 Hồi quy logistic là một mạng nơ-ron không có layer ẩn

Trước khi giới thiệu về cấu trúc của một mô hình mạng nơ-ron, chúng ta sẽ xem xét lại cách hồi quy logistic hoạt động. Sau đó, khi nhìn nhận cách vận hành của hổi quy logistic như một mạng nơ-ron đơn giản, bạn đọc sẽ có hình dung cụ thể hơn về cách xây dựng mô hình mạng nơ-ron.

Dữ liệu dùng để xây dựng mô hình hồi quy logistic được xây dựng dựa trên dữ liệu bao gồm 5 quan sát được cho trong bảng dưới đây

(#tab:unnamed-chunk-3)Dữ liệu cho hồi quy Logistic
Dữ liệu x1 x2 Màu sắc
A 1.0 2 blue
B 2.0 1 red
C 4.0 1 red
D 3.0 3 blue
E 1.5 3 blue
F 3.0 2 ?

Với mỗi điểm dữ liệu bất kỳ, giả sử là điểm A có với các thuộc tính $x_1(A) = $ và $x_2(A) = $, chúng ta sẽ thực hiện hai phép biến đổi kế tiếp nhau:

  • Phép biến đổi tuyển tính: với bộ 3 số thực bất kỳ \((b_0, b_1, b_2)\), chúng ta luôn có thể thực hiện phép biến đổi tuyến tính:

\[ A(1, x_1, x_2) \rightarrow b_0 \times 1 + b_1 \times x_1 + b_2 \times x_2 \] - Phép biến đổi phi tuyến tính: chúng ta sử dụng hàm số \(f(x) = sigmoid(x) = \cfrac{1}{1+e^{x}}\) để thực hiện phép biến đổi thứ hai

\[ b_0 + b_1 x_1 + b_2 x_2 \rightarrow \cfrac{1}{1+e^{b_0 + b_1 x_1 + b_2 x_2}} \] Sau khi thực hiện các phép biến đổi với từng điểm dữ liệu, chúng ta sẽ thu được với mỗi điểm dữ liệu một số nằm trong khoảng \((0,1)\).

(#tab:unnamed-chunk-4)Dữ liệu cho hồi quy Logistic
Dữ liệu \(x_1\) \(x_2\) Màu sắc Biến mục tiêu (\(y_i\)) Dữ liệu sau chuyển đổi (\(p_i\))
A 1.0 2 blue 1 \((1+exp(b_0+ b_1+2 b_2))^{-1}\)
B 2.0 1 red 0 \((1+exp(b_0+2 b_1+ b_2))^{-1}\)
C 4.0 1 red 0 \((1+exp(b_0+4 b_1+ b_2))^{-1}\)
D 3.0 3 blue 1 \((1+exp(b_0+3 b_1+3 b_2))^{-1}\)
E 1.5 3 blue 1 \((1+exp(b_0+1.5 b_1+3 b_2))^{-1}\)

Hàm tổn thất, tính bằng Cross Entropy qua năm điểm dữ liệu A, B, C, D, và E, sẽ là một hàm số của ba biến \((b_0, b_1, b_2)\) như sau

\[ Loss(b_0, b_1, b_2) = - \sum\limits_{Data = A}^E [y_i \times log(p_i) + (1-y_i) \times log(1-p_i)] \] với giá trị của \(y_i\)\(p_i\) được cho bởi bảng ở trên. Bằng thuật toán gradient descent, chúng ta có thể tính toán được giá trị của \((b_0, b_1, b_2)\) sao cho hàm \(Loss\) đạt giá trị nhỏ nhất bằng \((25,15,-30)\). Vậy có thể tính toán khả năng điểm \(F\) có màu xanh là \[ \mathbb{P}(F = blue) = (1+exp(25 + 3 \times 15 + 3 \times -30))^{-1} = 1 \] Như vậy, trong hổi quy logistic, chúng ta đã sử dụng 2 phép biến đổi dữ liệu bao gồm một phép biến đổi tuyến tính thông qua một véc-tơ \(b\) và sau đó là một phép biến đổi phi tuyến (hàm sigmoid) để thu được một giá trị duy nhất cho mỗi điểm dữ liệu. Quá trình ước lượng mô hình logistic là quá trình tìm kiếm các tham số \(b\) sao cho giá trị đầu ra sau các phép biến đổi của mỗi điểm dữ liệu gần với kết quả mong muốn nhất có thể.

13.2 Mạng neural với nhiều layer ẩn.

13.3 Ước lượng tham số cho mạng neural network

13.3.1 Backpropagation

13.3.2 Regularization and Stochastic Gradient Descent

13.4 Thực hành:

13.4.1 Sử dụng mạng neural network trong phân loại văn bản.

13.4.2 Sử dụng mạng neural network tích chập với dữ liệu \(mnist()\).